diff --git a/crates/base/src/input/base/element.rs b/crates/base/src/input/base/element.rs
index 5f82b2cf65..89e94a0f58 100644
--- a/crates/base/src/input/base/element.rs
+++ b/crates/base/src/input/base/element.rs
@@ -21,7 +21,7 @@ use crate::{
};
use super::{
- InputBaseState, TextDecoration,
+ GutterMarker, InputBaseState, InputEvent, RangeDecorationStyle, TextDecoration,
layout::{LastLayout, WhitespaceIndicators},
mode::LayoutMode,
};
@@ -51,6 +51,7 @@ pub(super) const RIGHT_MARGIN: Pixels = px(10.);
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
const FOLD_ICON_WIDTH: Pixels = px(14.);
const FOLD_ICON_HITBOX_WIDTH: Pixels = px(18.);
+pub(super) const GUTTER_MARKER_HITBOX_WIDTH: Pixels = px(22.);
const MAX_HIGHLIGHT_LINE_LENGTH: usize = 10_000;
const FOLD_CHEVRON_RIGHT_SVG: &[u8] = br#""#;
const FOLD_CHEVRON_DOWN_SVG: &[u8] = br#""#;
@@ -397,6 +398,10 @@ struct FoldIconLayout {
icons: Vec<(usize, bool, gpui::AnyElement)>,
}
+struct GutterMarkerLayout {
+ icons: Vec,
+}
+
pub(super) struct TextElement {
pub(crate) state: Entity>,
placeholder: SharedString,
@@ -794,6 +799,137 @@ impl TextElement {
builder.build().ok()
}
+ fn layout_range_corners(
+ range: &Range,
+ last_layout: &LastLayout,
+ ) -> Option>>> {
+ if range.is_empty()
+ || range.start < last_layout.visible_range_offset.start
+ || range.end > last_layout.visible_range_offset.end
+ {
+ return None;
+ }
+
+ let mut offset_y = last_layout.visible_top;
+ let mut corners = Vec::new();
+ for (line_offset, line) in last_layout
+ .visible_line_byte_offsets
+ .iter()
+ .zip(last_layout.lines.iter())
+ {
+ let line_size = line.size(last_layout.line_height);
+ let start = line.position_for_index(
+ range.start.saturating_sub(*line_offset),
+ last_layout,
+ false,
+ );
+ let end =
+ line.position_for_index(range.end.saturating_sub(*line_offset), last_layout, true);
+
+ if start.is_some() || end.is_some() {
+ let start = start
+ .unwrap_or_else(|| line.position_for_index(0, last_layout, false).unwrap());
+ let end = end.unwrap_or_else(|| {
+ line.position_for_index(line.len(), last_layout, false)
+ .unwrap()
+ });
+ let wrapped_lines = (end.y / last_layout.line_height).ceil() as usize
+ - (start.y / last_layout.line_height).ceil() as usize;
+ let mut end_x = end.x;
+ if wrapped_lines > 0 {
+ end_x = line_size.width;
+ }
+ end_x = end_x.max(start.x + px(6.));
+ let line_origin = point(px(0.), offset_y);
+ corners.push(Corners {
+ top_left: line_origin + point(start.x, start.y),
+ top_right: line_origin + point(end_x, start.y),
+ bottom_left: line_origin + point(start.x, start.y + last_layout.line_height),
+ bottom_right: line_origin + point(end_x, start.y + last_layout.line_height),
+ });
+
+ for index in 1..=wrapped_lines {
+ let start = point(
+ line.wrap_indent,
+ start.y + index as f32 * last_layout.line_height,
+ );
+ let mut wrapped_end = point(end.x, start.y);
+ if index < wrapped_lines {
+ wrapped_end.x = line_size.width;
+ }
+ corners.push(Corners {
+ top_left: line_origin + start,
+ top_right: line_origin + wrapped_end,
+ bottom_left: line_origin
+ + point(start.x, start.y + last_layout.line_height),
+ bottom_right: line_origin
+ + point(wrapped_end.x, wrapped_end.y + last_layout.line_height),
+ });
+ }
+ }
+
+ if start.is_some() && end.is_some() {
+ break;
+ }
+ offset_y += line_size.height;
+ }
+
+ for corners in &mut corners {
+ if corners.top_left.x > corners.top_right.x {
+ std::mem::swap(&mut corners.top_left, &mut corners.top_right);
+ std::mem::swap(&mut corners.bottom_left, &mut corners.bottom_right);
+ }
+ }
+ (!corners.is_empty()).then_some(corners)
+ }
+
+ fn layout_range_decorations(
+ &self,
+ last_layout: &LastLayout,
+ bounds: &Bounds,
+ cx: &App,
+ ) -> (Vec<(Path, Hsla)>, Vec<(Path, Hsla)>) {
+ let state = self.state.read(cx);
+ let mut fills = Vec::new();
+ let mut frames = Vec::new();
+ for decoration in state.extras.range_decorations() {
+ match decoration.style() {
+ RangeDecorationStyle::Fill => {
+ let color = decoration
+ .color()
+ .unwrap_or(state.editor_style.foreground.opacity(0.12));
+ if let Some(path) =
+ Self::layout_match_range(decoration.range().clone(), last_layout, bounds)
+ {
+ fills.push((path, color));
+ }
+ }
+ RangeDecorationStyle::Frame => {
+ let color = decoration.color().unwrap_or(state.editor_style.foreground);
+ let Some(corners) = Self::layout_range_corners(decoration.range(), last_layout)
+ else {
+ continue;
+ };
+ let points = frame_outline_points(&corners);
+ let Some(first) = points.first().copied() else {
+ continue;
+ };
+ let origin = bounds.origin + point(last_layout.line_number_width, px(0.));
+ let mut builder = gpui::PathBuilder::stroke(px(1.));
+ builder.move_to(origin + first);
+ for point in points.iter().skip(1) {
+ builder.line_to(origin + *point);
+ }
+ builder.close();
+ if let Ok(path) = builder.build() {
+ frames.push((path, color));
+ }
+ }
+ }
+ }
+ (fills, frames)
+ }
+
fn layout_search_matches(
&self,
last_layout: &LastLayout,
@@ -1018,6 +1154,9 @@ impl TextElement {
// Add extra space for fold icons
line_number_width += FOLD_ICON_HITBOX_WIDTH
}
+ if state.extras.gutter_lane_reserved() || !state.extras.gutter_markers().is_empty() {
+ line_number_width += GUTTER_MARKER_HITBOX_WIDTH;
+ }
(line_number_width, line_number_len)
}
@@ -1310,6 +1449,92 @@ impl TextElement {
icon_layout
}
+ fn layout_gutter_markers(
+ &self,
+ origin_x: Pixels,
+ bounds: &Bounds,
+ last_layout: &LastLayout,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> GutterMarkerLayout {
+ let (markers, renderer, marker_bounds_by_id) = {
+ let state = self.state.read(cx);
+ let Some(marker_bounds_by_id) = state.extras.gutter_marker_bounds() else {
+ return GutterMarkerLayout { icons: Vec::new() };
+ };
+ marker_bounds_by_id.borrow_mut().clear();
+ let Some(renderer) = state.extras.gutter_marker_renderer() else {
+ return GutterMarkerLayout { icons: Vec::new() };
+ };
+ let mut markers = state
+ .extras
+ .gutter_markers()
+ .iter()
+ .filter(|marker| {
+ last_layout
+ .visible_buffer_lines
+ .binary_search(&marker.logical_row())
+ .is_ok()
+ })
+ .cloned()
+ .collect::>();
+ markers.sort_by_key(GutterMarker::logical_row);
+ (markers, renderer, marker_bounds_by_id)
+ };
+
+ let mut icons = Vec::with_capacity(markers.len());
+ for marker in markers {
+ let Some(line_index) = last_layout
+ .visible_buffer_lines
+ .iter()
+ .position(|row| *row == marker.logical_row())
+ else {
+ continue;
+ };
+ let offset_y = last_layout.visible_top
+ + last_layout.lines[..line_index]
+ .iter()
+ .map(|line| line.size(last_layout.line_height).height)
+ .fold(px(0.), |height, line_height| height + line_height);
+ let marker_bounds = Bounds::new(
+ point(
+ origin_x + last_layout.line_number_width - GUTTER_MARKER_HITBOX_WIDTH,
+ bounds.origin.y + offset_y,
+ ),
+ size(GUTTER_MARKER_HITBOX_WIDTH, last_layout.line_height),
+ );
+ let child = renderer(&marker);
+ let marker_id = marker.id().clone();
+ let element_id = marker_id.clone();
+ marker_bounds_by_id
+ .borrow_mut()
+ .insert(marker_id.clone(), marker_bounds);
+ let logical_row = marker.logical_row();
+ let enabled = marker.is_enabled();
+ let state = self.state.clone();
+ let mut icon = gpui::div()
+ .id(element_id)
+ .size_full()
+ .child(child)
+ .on_mouse_down(MouseButton::Left, move |_, _, cx| {
+ cx.stop_propagation();
+ if enabled {
+ state.update(cx, |_, cx| {
+ cx.emit(InputEvent::GutterMarkerMouseDown {
+ marker_id: marker_id.clone(),
+ logical_row,
+ });
+ });
+ }
+ })
+ .into_any_element();
+ icon.prepaint_as_root(marker_bounds.origin, marker_bounds.size.into(), window, cx);
+ icons.push(icon);
+ }
+
+ GutterMarkerLayout { icons }
+ }
+
/// Paint fold icons using prepaint hitboxes.
///
/// This handles:
@@ -1336,6 +1561,12 @@ impl TextElement {
}
}
+ fn paint_gutter_markers(layout: &mut GutterMarkerLayout, window: &mut Window, cx: &mut App) {
+ for icon in &mut layout.icons {
+ icon.paint(window, cx);
+ }
+ }
+
#[allow(clippy::too_many_arguments)]
fn layout_lines(
state: &InputBaseState,
@@ -1634,11 +1865,14 @@ pub(super) struct PrepaintState {
hover_highlight_path: Option>,
search_match_paths: Vec<(Path, bool)>,
document_color_paths: Vec<(Path, Hsla)>,
+ range_decoration_fills: Vec<(Path, Hsla)>,
+ range_decoration_frames: Vec<(Path, Hsla)>,
hover_definition_hitbox: Option,
indent_guides_path: Option>,
bounds: Bounds,
/// Fold icon layout data
fold_icon_layout: FoldIconLayout,
+ gutter_marker_layout: GutterMarkerLayout,
// Inline completion rendering data
/// Shaped ghost lines to paint after cursor row (completion lines 2+)
ghost_lines: Vec,
@@ -1703,6 +1937,38 @@ fn print_points_as_svg_path(
}
}
}
+
+fn frame_outline_points(corners: &[Corners>]) -> Vec> {
+ let rects = corners
+ .iter()
+ .map(|corners| (corners.top_left, corners.bottom_right))
+ .collect::>();
+ let mut points = Vec::with_capacity(rects.len() * 4);
+ let first = rects[0];
+ let last = rects[rects.len() - 1];
+ points.push(first.0);
+ points.push(point(first.1.x, first.0.y));
+ for pair in rects.windows(2) {
+ let current = pair[0];
+ let next = pair[1];
+ points.push(current.1);
+ points.push(point(next.1.x, current.1.y));
+ points.push(point(next.1.x, next.1.y));
+ }
+ if points.last() != Some(&last.1) {
+ points.push(last.1);
+ }
+ points.push(point(last.0.x, last.1.y));
+ for pair in rects.windows(2).rev() {
+ let current = pair[1];
+ let next = pair[0];
+ points.push(point(current.0.x, current.0.y));
+ points.push(point(next.0.x, current.0.y));
+ points.push(point(next.0.x, next.0.y));
+ }
+ points
+}
+
impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
@@ -2068,6 +2334,8 @@ impl Element for TextElement {
let hover_highlight_path = self.layout_hover_highlight(&last_layout, &mut bounds, cx);
let document_color_paths =
self.layout_document_colors(&document_colors, &last_layout, &bounds, cx);
+ let (range_decoration_fills, range_decoration_frames) =
+ self.layout_range_decorations(&last_layout, &bounds, cx);
let state = self.state.read(cx);
let line_numbers = if state.mode.line_number() {
@@ -2134,6 +2402,8 @@ impl Element for TextElement {
)));
let fold_icon_layout =
self.layout_fold_icons(original_x, &bounds, &last_layout, window, cx);
+ let gutter_marker_layout =
+ self.layout_gutter_markers(original_x, &bounds, &last_layout, window, cx);
PrepaintState {
bounds,
@@ -2148,8 +2418,11 @@ impl Element for TextElement {
hover_highlight_path,
hover_definition_hitbox,
document_color_paths,
+ range_decoration_fills,
+ range_decoration_frames,
indent_guides_path,
fold_icon_layout,
+ gutter_marker_layout,
ghost_first_line,
ghost_lines,
ghost_lines_height,
@@ -2277,6 +2550,14 @@ impl Element for TextElement {
window.paint_path(path, editor_style.border.opacity(0.85));
}
+ // Application decorations sit below the user's selection.
+ for (path, color) in &prepaint.range_decoration_fills {
+ window.paint_path(path.clone(), *color);
+ }
+ for (path, color) in &prepaint.range_decoration_frames {
+ window.paint_path(path.clone(), *color);
+ }
+
// Paint selections
if window.is_window_active() {
let secondary_selection = Hsla {
@@ -2374,14 +2655,14 @@ impl Element for TextElement {
}
}
+ self.paint_inline_widgets(prepaint, origin, scroll_offset, text_align, window, cx);
+
// Paint blinking cursors (shared blink state for all carets)
if focused && show_cursor {
for cursor_info in prepaint.cursor_infos_with_scroll() {
window.paint_quad(fill(cursor_info.bounds, editor_style.caret));
}
- }
-
- // Paint line numbers
+ } // Paint line numbers
let mut offset_y = px(0.);
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
offset_y += invisible_top_padding;
@@ -2442,6 +2723,7 @@ impl Element for TextElement {
window,
cx,
);
+ Self::paint_gutter_markers(&mut prepaint.gutter_marker_layout, window, cx);
self.state.update(cx, |state, cx| {
let geometry_changed = state.last_bounds != Some(bounds)
@@ -2499,6 +2781,82 @@ impl Element for TextElement {
}
}
+impl TextElement {
+ fn paint_inline_widgets(
+ &self,
+ prepaint: &PrepaintState,
+ origin: Point,
+ scroll_offset: Pixels,
+ text_align: TextAlign,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let (widgets, color) = {
+ let state = self.state.read(cx);
+ (
+ state
+ .extras
+ .inline_widgets()
+ .iter()
+ .map(|widget| {
+ (
+ widget.clone(),
+ state.text.offset_to_point(widget.offset()).row,
+ )
+ })
+ .collect::>(),
+ state.editor_style.muted_foreground,
+ )
+ };
+ if widgets.is_empty() {
+ return;
+ }
+ let text_style = window.text_style();
+ let text_size = text_style.font_size.to_pixels(window.rem_size());
+ for (widget, row) in &widgets {
+ let Ok(line_index) = prepaint.last_layout.visible_buffer_lines.binary_search(row)
+ else {
+ continue;
+ };
+ let line = &prepaint.last_layout.lines[line_index];
+ let line_offset = prepaint.last_layout.visible_line_byte_offsets[line_index];
+ let Some(widget_point) = line.position_for_index(
+ widget.offset().saturating_sub(line_offset),
+ &prepaint.last_layout,
+ false,
+ ) else {
+ continue;
+ };
+ let y = prepaint.last_layout.visible_top
+ + prepaint.last_layout.lines[..line_index]
+ .iter()
+ .map(|line| line.size(prepaint.last_layout.line_height).height)
+ .fold(px(0.), |height, line_height| height + line_height);
+ let position = widget_point + gpui::point(prepaint.last_layout.line_number_width, y);
+ let text = widget.text().clone();
+ let run = TextRun {
+ len: text.len(),
+ font: text_style.font(),
+ color,
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+ let shaped = window
+ .text_system()
+ .shape_line(text, text_size, &[run], None);
+ let _ = shaped.paint(
+ origin + position + point(scroll_offset, px(0.)),
+ prepaint.last_layout.line_height,
+ text_align,
+ None,
+ window,
+ cx,
+ );
+ }
+ }
+}
+
/// Split placeholder text into display lines and trim runs to each line.
fn placeholder_line_runs<'a>(
display_text: &'a str,
@@ -2737,6 +3095,39 @@ fn split_runs_by_bg_segments(
mod tests {
use super::*;
+ #[test]
+ fn frame_outline_is_continuous_across_different_line_widths() {
+ let corners = [
+ Corners {
+ top_left: point(px(2.), px(0.)),
+ top_right: point(px(20.), px(0.)),
+ bottom_left: point(px(2.), px(10.)),
+ bottom_right: point(px(20.), px(10.)),
+ },
+ Corners {
+ top_left: point(px(0.), px(10.)),
+ top_right: point(px(12.), px(10.)),
+ bottom_left: point(px(0.), px(20.)),
+ bottom_right: point(px(12.), px(20.)),
+ },
+ ];
+
+ assert_eq!(
+ frame_outline_points(&corners),
+ vec![
+ point(px(2.), px(0.)),
+ point(px(20.), px(0.)),
+ point(px(20.), px(10.)),
+ point(px(12.), px(10.)),
+ point(px(12.), px(20.)),
+ point(px(0.), px(20.)),
+ point(px(0.), px(10.)),
+ point(px(2.), px(10.)),
+ point(px(2.), px(0.)),
+ ]
+ );
+ }
+
#[test]
fn test_plain_text_decorations_include_unstyled_gaps() {
let decoration = HighlightStyle {
diff --git a/crates/base/src/input/base/kind.rs b/crates/base/src/input/base/kind.rs
index 6308a7af00..5791b711d0 100644
--- a/crates/base/src/input/base/kind.rs
+++ b/crates/base/src/input/base/kind.rs
@@ -21,15 +21,17 @@
//! reachable.
use std::cell::RefCell;
+use std::collections::HashMap;
use std::rc::Rc;
-use gpui::{Div, Entity, Stateful, Window};
+use gpui::{Bounds, Div, Entity, Pixels, SharedString, Stateful, Window};
use ropey::Rope;
-use super::decorations::DecorationCollections;
+use super::decorations::{DecorationCollections, EditorAnnotations};
use super::lsp::{ContextMenuContent, HoverDefinition, InlineCompletion};
use crate::input::{
- HighlightStyleResolver, InputEdit, InputHighlighter, SyntaxContext, TextDecoration,
+ GutterMarker, HighlightStyleResolver, InlineWidget, InputEdit, InputHighlighter,
+ RangeDecoration, SyntaxContext, TextDecoration,
};
use crate::input::{HoverPopoverState, Lsp};
use gpui::Task;
@@ -115,6 +117,36 @@ pub trait InputExtras: Default + 'static {
fn context_menu_capabilities(&self) -> (bool, bool) {
(false, false)
}
+
+ /// Feature-owned gutter markers anchored to logical rows.
+ fn gutter_markers(&self) -> &[GutterMarker] {
+ &[]
+ }
+
+ /// Whether the gutter marker lane has ever been used and stays reserved.
+ fn gutter_lane_reserved(&self) -> bool {
+ false
+ }
+
+ /// The application-owned presentation for gutter markers, if any.
+ fn gutter_marker_renderer(&self) -> Option {
+ None
+ }
+
+ /// Last-paint bounds per gutter marker, keyed by marker id.
+ fn gutter_marker_bounds(&self) -> Option>>>> {
+ None
+ }
+
+ /// Geometric range decorations to paint.
+ fn range_decorations(&self) -> &[RangeDecoration] {
+ &[]
+ }
+
+ /// Non-document inline widgets to paint at their offsets.
+ fn inline_widgets(&self) -> &[InlineWidget] {
+ &[]
+ }
}
/// A mode with nothing extra to render.
@@ -203,6 +235,9 @@ pub trait InputModeKind: sealed::Sealed + Sized + 'static {
) {
}
+ /// Records a successful document mutation after validation and normalization.
+ fn document_did_change(_state: &mut InputBaseState) {}
+
/// Refreshes language-server state after the text changed.
fn refresh_language_features(
_state: &mut InputBaseState,
@@ -341,6 +376,7 @@ pub struct EditorExtras {
pub(crate) hover_popover: Option,
pub(crate) hover_definition: HoverDefinition,
pub(crate) context_menu_task: Task>,
+ pub(crate) annotations: EditorAnnotations,
}
impl Default for EditorExtras {
@@ -353,6 +389,7 @@ impl Default for EditorExtras {
hover_popover: None,
hover_definition: HoverDefinition::default(),
context_menu_task: Task::ready(Ok(())),
+ annotations: EditorAnnotations::default(),
}
}
}
diff --git a/crates/base/src/input/base/state.rs b/crates/base/src/input/base/state.rs
index 56603df8b0..145fd89cc9 100644
--- a/crates/base/src/input/base/state.rs
+++ b/crates/base/src/input/base/state.rs
@@ -121,9 +121,16 @@ actions!(
#[derive(Clone)]
pub enum InputEvent {
Change,
- PressEnter { secondary: bool, shift: bool },
+ PressEnter {
+ secondary: bool,
+ shift: bool,
+ },
Focus,
Blur,
+ GutterMarkerMouseDown {
+ marker_id: SharedString,
+ logical_row: usize,
+ },
}
pub(super) const CONTEXT: &str = "Input";
@@ -3485,6 +3492,7 @@ impl InputBaseState {
self.text.replace(range.clone(), new_text);
M::adjust_annotations(self, range, new_text.len());
+ M::document_did_change(self);
recorded |= self.push_history(
&old_text,
range,
@@ -3830,12 +3838,16 @@ impl EntityInputHandler for InputBaseState {
}
}
- if mask_changed {
- // Masking rewrites the whole document, so ranges recorded against
- // the old text no longer point at anything.
- M::reset_annotations(self);
- } else {
- M::adjust_annotations(self, &range, new_text.len());
+ let document_changed = old_text != self.text;
+ if document_changed {
+ if mask_changed {
+ // Masking rewrites the whole document, so ranges recorded against
+ // the old text no longer point at anything.
+ M::reset_annotations(self);
+ } else {
+ M::adjust_annotations(self, &range, new_text.len());
+ }
+ M::document_did_change(self);
}
if mask_changed {
// A segment-based history entry no longer matches the masked
@@ -3965,7 +3977,10 @@ impl EntityInputHandler for InputBaseState {
}
}
- M::adjust_annotations(self, &range, new_text.len());
+ if old_text != self.text {
+ M::adjust_annotations(self, &range, new_text.len());
+ M::document_did_change(self);
+ }
if let Some(diagnostics) = self.mode.diagnostics_mut() {
diagnostics.reset(&self.text)
}
diff --git a/crates/base/src/input/editor/decorations.rs b/crates/base/src/input/editor/decorations.rs
index dc4b393cce..4f612e1ec1 100644
--- a/crates/base/src/input/editor/decorations.rs
+++ b/crates/base/src/input/editor/decorations.rs
@@ -1,12 +1,206 @@
use crate::input::EditorMode;
+use std::cell::RefCell;
+use std::collections::HashMap;
use std::ops::Range;
+use std::rc::Rc;
-use gpui::{App, Context, HighlightStyle, WeakEntity};
+use gpui::{
+ AnyElement, App, Bounds, Context, HighlightStyle, Hsla, Pixels, SharedString, WeakEntity,
+};
use ropey::Rope;
use sum_tree::Bias;
use super::{InputBaseState, RopeExt as _};
+/// A feature-owned marker anchored to a logical editor row.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct GutterMarker {
+ id: SharedString,
+ logical_row: usize,
+ icon: SharedString,
+ tooltip: Option,
+ enabled: bool,
+}
+
+impl GutterMarker {
+ pub fn new(
+ id: impl Into,
+ logical_row: usize,
+ icon: impl Into,
+ ) -> Self {
+ Self {
+ id: id.into(),
+ logical_row,
+ icon: icon.into(),
+ tooltip: None,
+ enabled: true,
+ }
+ }
+
+ pub fn id(&self) -> &SharedString {
+ &self.id
+ }
+
+ pub fn logical_row(&self) -> usize {
+ self.logical_row
+ }
+
+ pub fn icon(&self) -> &SharedString {
+ &self.icon
+ }
+
+ pub fn tooltip(&self) -> Option<&SharedString> {
+ self.tooltip.as_ref()
+ }
+
+ pub fn is_enabled(&self) -> bool {
+ self.enabled
+ }
+
+ pub fn with_tooltip(mut self, tooltip: impl Into) -> Self {
+ self.tooltip = Some(tooltip.into());
+ self
+ }
+
+ pub fn enabled(mut self, enabled: bool) -> Self {
+ self.enabled = enabled;
+ self
+ }
+}
+
+/// Application-owned presentation for a gutter marker.
+pub type GutterMarkerRenderer = std::rc::Rc AnyElement>;
+
+/// Geometric presentation for an editor range decoration.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum RangeDecorationStyle {
+ /// Fill the continuous visual range.
+ Fill,
+ /// Draw a continuous one-pixel frame around the visual range.
+ #[default]
+ Frame,
+}
+
+/// A geometric decoration over a UTF-8 byte range.
+#[derive(Clone, Debug, PartialEq)]
+pub struct RangeDecoration {
+ id: SharedString,
+ range: Range,
+ style: RangeDecorationStyle,
+ color: Option,
+}
+
+impl RangeDecoration {
+ pub fn new(id: impl Into, range: Range) -> Self {
+ Self {
+ id: id.into(),
+ range,
+ style: RangeDecorationStyle::default(),
+ color: None,
+ }
+ }
+
+ pub fn id(&self) -> &SharedString {
+ &self.id
+ }
+
+ pub fn range(&self) -> &Range {
+ &self.range
+ }
+
+ pub fn style(&self) -> RangeDecorationStyle {
+ self.style
+ }
+
+ pub fn color(&self) -> Option {
+ self.color
+ }
+
+ pub fn with_style(mut self, style: RangeDecorationStyle) -> Self {
+ self.style = style;
+ self
+ }
+
+ pub fn with_color(mut self, color: Hsla) -> Self {
+ self.color = Some(color);
+ self
+ }
+}
+
+/// Non-document text painted at a UTF-8 byte offset.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct InlineWidget {
+ id: SharedString,
+ offset: usize,
+ text: SharedString,
+}
+
+impl InlineWidget {
+ pub fn new(id: impl Into, offset: usize, text: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ offset,
+ text: text.into(),
+ }
+ }
+
+ pub fn id(&self) -> &SharedString {
+ &self.id
+ }
+
+ pub fn offset(&self) -> usize {
+ self.offset
+ }
+
+ pub fn text(&self) -> &SharedString {
+ &self.text
+ }
+}
+
+#[derive(Default)]
+pub(crate) struct EditorAnnotations {
+ pub(crate) document_revision: u64,
+ pub(crate) completion_epoch: u64,
+ pub(crate) gutter_markers: Vec,
+ pub(crate) gutter_lane_reserved: bool,
+ pub(crate) gutter_marker_bounds: Rc>>>,
+ pub(crate) range_decorations: Vec,
+ pub(crate) inline_widgets: Vec,
+ pub(crate) gutter_marker_renderer: Option,
+}
+
+impl EditorAnnotations {
+ pub(crate) fn adjust_for_edit(&mut self, edited_range: &Range, inserted_len: usize) {
+ for decoration in &mut self.range_decorations {
+ decoration.range = adjust_range_for_edit(&decoration.range, edited_range, inserted_len);
+ }
+ self.range_decorations
+ .retain(|decoration| !decoration.range.is_empty());
+ for widget in &mut self.inline_widgets {
+ widget.offset = adjust_offset_for_edit(widget.offset, edited_range, inserted_len);
+ }
+ }
+}
+
+fn adjust_offset_for_edit(
+ offset: usize,
+ edited_range: &Range,
+ inserted_len: usize,
+) -> usize {
+ if offset <= edited_range.start {
+ return offset;
+ }
+ if offset < edited_range.end {
+ return edited_range.start.saturating_add(inserted_len);
+ }
+ let removed_len = edited_range.end.saturating_sub(edited_range.start);
+ if inserted_len >= removed_len {
+ offset.saturating_add(inserted_len - removed_len)
+ } else {
+ offset.saturating_sub(removed_len - inserted_len)
+ }
+}
+
/// A presentation style applied to a UTF-8 byte range in an input.
///
/// This is the GPUI [`HighlightStyle`] counterpart of Monaco's
@@ -248,6 +442,109 @@ impl InputBaseState {
id,
}
}
+
+ /// Monotonic content revision. Selection, focus and scrolling do not change it.
+ pub fn document_revision(&self) -> u64 {
+ self.extras.annotations.document_revision
+ }
+
+ /// Replace all gutter markers. The marker lane remains reserved after first use.
+ pub fn set_gutter_markers(&mut self, markers: Vec, cx: &mut Context) {
+ self.extras.annotations.gutter_markers = markers;
+ self.extras.annotations.gutter_lane_reserved = true;
+ cx.notify();
+ }
+
+ pub fn clear_gutter_markers(&mut self, cx: &mut Context) {
+ if !self.extras.annotations.gutter_markers.is_empty() {
+ self.extras.annotations.gutter_markers.clear();
+ cx.notify();
+ }
+ }
+
+ pub fn gutter_markers(&self) -> &[GutterMarker] {
+ &self.extras.annotations.gutter_markers
+ }
+
+ pub fn gutter_marker_bounds(&self, id: &str) -> Option> {
+ self.extras
+ .annotations
+ .gutter_marker_bounds
+ .borrow()
+ .get(id)
+ .copied()
+ }
+
+ pub fn set_gutter_marker_renderer(
+ &mut self,
+ renderer: GutterMarkerRenderer,
+ cx: &mut Context,
+ ) {
+ self.extras.annotations.gutter_marker_renderer = Some(renderer);
+ cx.notify();
+ }
+
+ #[doc(hidden)]
+ pub fn project_gutter_marker_renderer(&mut self, renderer: GutterMarkerRenderer) {
+ self.extras.annotations.gutter_marker_renderer = Some(renderer);
+ }
+
+ pub fn set_range_decorations(
+ &mut self,
+ decorations: Vec,
+ cx: &mut Context,
+ ) {
+ self.extras.annotations.range_decorations = normalize_ranges(&self.text, decorations);
+ cx.notify();
+ }
+
+ pub fn clear_range_decorations(&mut self, cx: &mut Context) {
+ if !self.extras.annotations.range_decorations.is_empty() {
+ self.extras.annotations.range_decorations.clear();
+ cx.notify();
+ }
+ }
+
+ pub fn range_decorations(&self) -> &[RangeDecoration] {
+ &self.extras.annotations.range_decorations
+ }
+
+ pub fn set_inline_widgets(&mut self, widgets: Vec, cx: &mut Context) {
+ self.extras.annotations.inline_widgets = normalize_widgets(&self.text, widgets);
+ cx.notify();
+ }
+
+ pub fn clear_inline_widgets(&mut self, cx: &mut Context) {
+ if !self.extras.annotations.inline_widgets.is_empty() {
+ self.extras.annotations.inline_widgets.clear();
+ cx.notify();
+ }
+ }
+
+ pub fn inline_widgets(&self) -> &[InlineWidget] {
+ &self.extras.annotations.inline_widgets
+ }
+}
+
+fn normalize_ranges(text: &Rope, decorations: Vec) -> Vec {
+ decorations
+ .into_iter()
+ .filter_map(|mut decoration| {
+ decoration.range = text.clip_offset(decoration.range.start, Bias::Left)
+ ..text.clip_offset(decoration.range.end, Bias::Right);
+ (!decoration.range.is_empty()).then_some(decoration)
+ })
+ .collect()
+}
+
+fn normalize_widgets(text: &Rope, widgets: Vec) -> Vec {
+ widgets
+ .into_iter()
+ .map(|mut widget| {
+ widget.offset = text.clip_offset(widget.offset, Bias::Left);
+ widget
+ })
+ .collect()
}
#[cfg(test)]
@@ -333,4 +630,31 @@ mod tests {
assert_eq!(adjust_range_for_edit(&(2..6), &(6..6), 2), 2..6);
assert_eq!(adjust_range_for_edit(&(2..6), &(2..6), 3), 2..5);
}
+
+ #[test]
+ fn geometric_decorations_and_widgets_follow_utf8_edits() {
+ let mut annotations = EditorAnnotations {
+ range_decorations: vec![RangeDecoration::new("range", 2..6)],
+ inline_widgets: vec![InlineWidget::new("hint", 6, "hint")],
+ ..Default::default()
+ };
+
+ annotations.adjust_for_edit(&(0..0), "é".len());
+ assert_eq!(annotations.range_decorations[0].range(), &(4..8));
+ assert_eq!(annotations.inline_widgets[0].offset(), 8);
+
+ annotations.adjust_for_edit(&(5..7), 1);
+ assert_eq!(annotations.range_decorations[0].range(), &(4..7));
+ assert_eq!(annotations.inline_widgets[0].offset(), 7);
+ }
+
+ #[test]
+ fn extension_ranges_and_offsets_clip_to_utf8_boundaries() {
+ let text = Rope::from("éx");
+ let decorations = normalize_ranges(&text, vec![RangeDecoration::new("range", 1..3)]);
+ let widgets = normalize_widgets(&text, vec![InlineWidget::new("hint", 1, "hint")]);
+
+ assert_eq!(decorations[0].range(), &(0..3));
+ assert_eq!(widgets[0].offset(), 0);
+ }
}
diff --git a/crates/base/src/input/editor/lsp/completions.rs b/crates/base/src/input/editor/lsp/completions.rs
index 5e3113129c..ce70e5783c 100644
--- a/crates/base/src/input/editor/lsp/completions.rs
+++ b/crates/base/src/input/editor/lsp/completions.rs
@@ -8,12 +8,33 @@ use lsp_types::{
};
use ropey::Rope;
use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
+use sum_tree::Bias;
-use crate::input::InputBaseState;
+use crate::input::{InputBaseState, RopeExt as _};
/// Default debounce duration for inline completions.
const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
+#[allow(clippy::too_many_arguments)]
+fn completion_context_is_current(
+ current_epoch: u64,
+ request_epoch: u64,
+ current_revision: u64,
+ request_revision: u64,
+ current_cursor: usize,
+ request_cursor: usize,
+ current_start: Option,
+ request_start: usize,
+ current_query: &str,
+ request_query: &str,
+) -> bool {
+ current_epoch == request_epoch
+ && current_revision == request_revision
+ && current_cursor == request_cursor
+ && current_start == Some(request_start)
+ && current_query == request_query
+}
+
/// Display options for the LSP completion popover.
///
/// Accessed through [`super::Lsp::completion_menu`] so embedders can tweak the
@@ -125,6 +146,39 @@ impl InputBaseState {
new_text: &str,
window: &mut Window,
cx: &mut Context,
+ ) {
+ self.request_completion(range, new_text, false, window, cx);
+ }
+
+ /// Re-run popup completion at the current cursor without changing the document.
+ ///
+ /// This is useful when an asynchronous metadata source becomes ready after
+ /// the original request. Newline, tab, and line-start positions are ignored.
+ pub fn refresh_completion_popup(&mut self, window: &mut Window, cx: &mut Context) {
+ if self.completion_inserting {
+ return;
+ }
+ let cursor = self.cursor();
+ let start = self.text.clip_offset(cursor.saturating_sub(1), Bias::Left);
+ let Some(last_char) = self.text.char_at(start) else {
+ return;
+ };
+ if !(last_char.is_ascii_alphanumeric()
+ || matches!(last_char, '_' | '.' | ' ' | ')' | ']' | '"' | '\''))
+ {
+ return;
+ }
+ let text = self.text.slice(start..cursor).to_string();
+ self.request_completion(&(start..start), &text, true, window, cx);
+ }
+
+ fn request_completion(
+ &mut self,
+ range: &Range,
+ new_text: &str,
+ force: bool,
+ window: &mut Window,
+ cx: &mut Context,
) {
if self.completion_inserting {
return;
@@ -141,7 +195,7 @@ impl InputBaseState {
let start = range.end;
let new_offset = self.cursor();
- if !provider.is_completion_trigger(start, new_text, cx) {
+ if !force && !provider.is_completion_trigger(start, new_text, cx) {
return;
}
@@ -175,10 +229,17 @@ impl InputBaseState {
.clone_from(&query);
let completion_context = CompletionContext {
- trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
+ trigger_kind: if force {
+ lsp_types::CompletionTriggerKind::INVOKED
+ } else {
+ lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER
+ },
trigger_character: Some(query),
};
+ let request_id = self.next_completion_request_id();
+ let document_revision = self.document_revision();
+ let query = self.extras.context_menu_content.completion.query.clone();
let provider_responses =
provider.completions(&self.text, new_offset, completion_context, window, cx);
self.extras.context_menu_task = cx.spawn_in(window, async move |editor, cx| {
@@ -191,7 +252,17 @@ impl InputBaseState {
}
if completions.is_empty() {
- editor.update(cx, |editor, cx| {
+ editor.update_in(cx, |editor, window, cx| {
+ if !editor.completion_request_is_current(
+ request_id,
+ document_revision,
+ new_offset,
+ start_offset,
+ &query,
+ window,
+ ) {
+ return;
+ }
editor.extras.context_menu_content.completion.open = false;
editor.extras.context_menu_content.completion.items.clear();
editor.extras.context_menu_content.completion.bump();
@@ -202,7 +273,14 @@ impl InputBaseState {
editor
.update_in(cx, |editor, window, cx| {
- if !editor.focus_handle.is_focused(window) {
+ if !editor.completion_request_is_current(
+ request_id,
+ document_revision,
+ new_offset,
+ start_offset,
+ &query,
+ window,
+ ) {
return;
}
@@ -223,6 +301,39 @@ impl InputBaseState {
});
}
+ fn next_completion_request_id(&mut self) -> u64 {
+ self.extras.annotations.completion_epoch =
+ self.extras.annotations.completion_epoch.saturating_add(1);
+ self.extras.annotations.completion_epoch
+ }
+
+ fn completion_request_is_current(
+ &self,
+ request_id: u64,
+ document_revision: u64,
+ cursor: usize,
+ start_offset: usize,
+ query: &str,
+ window: &Window,
+ ) -> bool {
+ self.focus_handle.is_focused(window)
+ && completion_context_is_current(
+ self.extras.annotations.completion_epoch,
+ request_id,
+ self.document_revision(),
+ document_revision,
+ self.cursor(),
+ cursor,
+ self.extras
+ .context_menu_content
+ .completion
+ .trigger_start_offset,
+ start_offset,
+ &self.extras.context_menu_content.completion.query,
+ query,
+ )
+ }
+
pub(crate) fn hide_context_menu(&mut self, cx: &mut Context) {
self.extras.context_menu_content.completion.open = false;
self.extras.context_menu_content.code_action.open = false;
@@ -283,6 +394,8 @@ impl InputBaseState {
let offset = self.cursor();
let text = self.text.clone();
+ let request_id = self.next_completion_request_id();
+ let document_revision = self.document_revision();
let debounce = provider.inline_completion_debounce();
let background_executor = cx.background_executor().clone();
@@ -293,7 +406,11 @@ impl InputBaseState {
// Now fetch the inline completion after the debounce period
let task = editor.update_in(cx, |editor, window, cx| {
// Check if cursor has moved during debounce
- if editor.cursor() != offset {
+ if editor.extras.annotations.completion_epoch != request_id
+ || editor.document_revision() != document_revision
+ || editor.cursor() != offset
+ || editor.text != text
+ {
return None;
}
@@ -318,7 +435,11 @@ impl InputBaseState {
editor.update_in(cx, |editor, _window, cx| {
// Only apply if cursor still hasn't moved
- if editor.cursor() != offset {
+ if editor.extras.annotations.completion_epoch != request_id
+ || editor.document_revision() != document_revision
+ || editor.cursor() != offset
+ || editor.text != text
+ {
return;
}
@@ -370,3 +491,22 @@ impl InputBaseState {
true
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::completion_context_is_current;
+
+ #[test]
+ fn completion_context_rejects_every_stale_dimension() {
+ let current = |epoch, revision, cursor, start, query: &str| {
+ completion_context_is_current(epoch, 7, revision, 11, cursor, 5, start, 2, query, "abc")
+ };
+
+ assert!(current(7, 11, 5, Some(2), "abc"));
+ assert!(!current(8, 11, 5, Some(2), "abc"));
+ assert!(!current(7, 12, 5, Some(2), "abc"));
+ assert!(!current(7, 11, 6, Some(2), "abc"));
+ assert!(!current(7, 11, 5, Some(1), "abc"));
+ assert!(!current(7, 11, 5, Some(2), "abcd"));
+ }
+}
diff --git a/crates/base/src/input/editor/mod.rs b/crates/base/src/input/editor/mod.rs
index 27d0b77928..ed173bf466 100644
--- a/crates/base/src/input/editor/mod.rs
+++ b/crates/base/src/input/editor/mod.rs
@@ -50,6 +50,15 @@ impl InputModeKind for EditorMode {
new_len: usize,
) {
state.extras.decorations.adjust_for_edit(range, new_len);
+ state.extras.annotations.adjust_for_edit(range, new_len);
+ }
+
+ fn document_did_change(state: &mut InputBaseState) {
+ state.extras.annotations.document_revision =
+ state.extras.annotations.document_revision.saturating_add(1);
+ state.extras.annotations.completion_epoch =
+ state.extras.annotations.completion_epoch.saturating_add(1);
+ state.extras.inline_completion.item = None;
}
fn refresh_language_features(
@@ -251,4 +260,36 @@ impl crate::input::InputExtras for super::EditorExtras {
!self.lsp.code_action_providers.is_empty(),
)
}
+
+ fn gutter_markers(&self) -> &[super::GutterMarker] {
+ &self.annotations.gutter_markers
+ }
+
+ fn gutter_lane_reserved(&self) -> bool {
+ self.annotations.gutter_lane_reserved
+ }
+
+ fn gutter_marker_renderer(&self) -> Option {
+ self.annotations.gutter_marker_renderer.clone()
+ }
+
+ fn gutter_marker_bounds(
+ &self,
+ ) -> Option<
+ std::rc::Rc<
+ std::cell::RefCell<
+ std::collections::HashMap>,
+ >,
+ >,
+ > {
+ Some(self.annotations.gutter_marker_bounds.clone())
+ }
+
+ fn range_decorations(&self) -> &[super::RangeDecoration] {
+ &self.annotations.range_decorations
+ }
+
+ fn inline_widgets(&self) -> &[super::InlineWidget] {
+ &self.annotations.inline_widgets
+ }
}
diff --git a/crates/base/src/input/mod.rs b/crates/base/src/input/mod.rs
index 11a47b3693..0e973d53d3 100644
--- a/crates/base/src/input/mod.rs
+++ b/crates/base/src/input/mod.rs
@@ -70,7 +70,10 @@ pub(crate) fn init(cx: &mut App) {
pub use crate::number_input::{NumberInputEvent, NumberStep};
pub use base::{InputBase, InputContextMenuCapabilities, InputStyles};
pub use cursor::Selection;
-pub use decorations::{TextDecoration, TextDecorationCollection};
+pub use decorations::{
+ GutterMarker, GutterMarkerRenderer, InlineWidget, RangeDecoration, RangeDecorationStyle,
+ TextDecoration, TextDecorationCollection,
+};
pub use diagnostics::{
Diagnostic, DiagnosticEntry, DiagnosticRelatedInformation, DiagnosticSet, DiagnosticSeverity,
DiagnosticSummary, DiagnosticTag, RelatedInformation,
diff --git a/crates/component/src/input/editor.rs b/crates/component/src/input/editor.rs
index 3f279d3a61..ff8688faa4 100644
--- a/crates/component/src/input/editor.rs
+++ b/crates/component/src/input/editor.rs
@@ -1,12 +1,14 @@
use std::rc::Rc;
use gpui::{
- App, DefiniteLength, Entity, IntoElement, RenderOnce, SharedString, StyleRefinement, Styled,
- Window, prelude::FluentBuilder as _, relative,
+ App, DefiniteLength, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
+ RenderOnce, SharedString, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
+ relative,
};
use super::{EditorState, Input};
use crate::native_menu::NativeMenu;
+use crate::tooltip::{ManagedTooltipExt as _, Tooltip};
use crate::{ActiveTheme as _, RoleOverride, StyledExt as _};
/// A code editor takes its rows from the font, so that a smaller or larger
@@ -26,6 +28,7 @@ pub struct Editor {
tab_index: isize,
role: RoleOverride,
aria_label: Option,
+ gutter_marker_renderer: Option,
/// An optional context menu builder to allow a custom context menu.
///
@@ -46,6 +49,23 @@ impl Editor {
tab_index: 0,
role: RoleOverride::default(),
aria_label: None,
+ gutter_marker_renderer: Some(Rc::new(|marker| {
+ div()
+ .id(marker.id().clone())
+ .size_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .text_xs()
+ .when(!marker.is_enabled(), |this| this.opacity(0.5))
+ .child(marker.icon().clone())
+ .when_some(marker.tooltip().cloned(), |this, tooltip| {
+ this.managed_tooltip(move |window, cx| {
+ Tooltip::new(tooltip.clone()).build(window, cx)
+ })
+ })
+ .into_any_element()
+ })),
context_menu_builder: None,
}
}
@@ -106,6 +126,15 @@ impl Editor {
self.context_menu_builder = Some(Rc::new(f));
self
}
+
+ /// Render application-defined gutter marker icon tokens.
+ pub fn gutter_marker_renderer(
+ mut self,
+ renderer: impl Fn(&crate::input::GutterMarker) -> gpui::AnyElement + 'static,
+ ) -> Self {
+ self.gutter_marker_renderer = Some(Rc::new(renderer));
+ self
+ }
}
impl Styled for Editor {
@@ -116,6 +145,11 @@ impl Styled for Editor {
impl RenderOnce for Editor {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+ if let Some(renderer) = self.gutter_marker_renderer.clone() {
+ self.state.update(cx, |state, _| {
+ state.project_gutter_marker_renderer(renderer);
+ });
+ }
Input::from_state(self.state.clone())
// Source code wants a monospace font at a code size, and rows that
// follow that size. These come first so that a text style set on
@@ -145,8 +179,7 @@ mod tests {
use super::*;
use crate::input::EditorState;
use gpui::{
- AppContext as _, Context, ParentElement as _, Pixels, Render, TestAppContext,
- VisualTestContext, div, px,
+ AppContext as _, Context, Pixels, Render, TestAppContext, VisualTestContext, div, px,
};
struct Harness {
diff --git a/crates/component/src/input/mod.rs b/crates/component/src/input/mod.rs
index becfdac527..fdf028e664 100644
--- a/crates/component/src/input/mod.rs
+++ b/crates/component/src/input/mod.rs
@@ -21,15 +21,16 @@ pub use gpui_base::input::{
CompletionProvider, Copy, Cut, DefinitionProvider, Delete, DeleteToBeginningOfLine,
DeleteToEndOfLine, DeleteToNextWordEnd, DeleteToPreviousWordStart, DisplayMap, DisplayPoint,
DocumentColorProvider, DocumentRangeSemanticTokensProvider, EditorState, Enter, Escape,
- FoldRange, GoToDefinition, HighlightStyleResolver, HoverPopoverState, HoverProvider, Indent,
- IndentInline, InputEdit, InputEvent, InputHighlighter, InputHighlighterFactory, InputState,
- Lsp, MaskPattern, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight,
- MoveToEnd, MoveToEndOfLine, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveToStartOfLine,
- MoveUp, Outdent, OutdentInline, Paste, Point, Redo, Replace, Rope, RopeExt, RopeLines, Search,
- SelectAll, SelectToEnd, SelectToEndOfLine, SelectToNextWordEnd, SelectToPreviousWordStart,
- SelectToStart, SelectToStartOfLine, Selection, ShowCharacterPalette, ShowDocumentHandler,
- TabSize, TextDecoration, TextDecorationCollection, TextareaState, ToggleCodeActions, Undo,
- WrappingIndent,
+ FoldRange, GoToDefinition, GutterMarker, GutterMarkerRenderer, HighlightStyleResolver,
+ HoverPopoverState, HoverProvider, Indent, IndentInline, InlineWidget, InputEdit, InputEvent,
+ InputHighlighter, InputHighlighterFactory, InputState, Lsp, MaskPattern, MoveDown, MoveEnd,
+ MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight, MoveToEnd, MoveToEndOfLine,
+ MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveToStartOfLine, MoveUp, Outdent,
+ OutdentInline, Paste, Point, RangeDecoration, RangeDecorationStyle, Redo, Replace, Rope,
+ RopeExt, RopeLines, Search, SelectAll, SelectToEnd, SelectToEndOfLine, SelectToNextWordEnd,
+ SelectToPreviousWordStart, SelectToStart, SelectToStartOfLine, Selection, ShowCharacterPalette,
+ ShowDocumentHandler, TabSize, TextDecoration, TextDecorationCollection, TextareaState,
+ ToggleCodeActions, Undo, WrappingIndent,
};
pub use gpui_base::input::{EditorMode, InputMode, InputModeKind, TextareaMode};
#[doc(hidden)]
diff --git a/crates/shell/src/engine/quickjs/mod.rs b/crates/shell/src/engine/quickjs/mod.rs
index 954e139a31..85f632fe2d 100644
--- a/crates/shell/src/engine/quickjs/mod.rs
+++ b/crates/shell/src/engine/quickjs/mod.rs
@@ -3776,7 +3776,10 @@ impl ShellRuntime {
payload.set("secondary", *secondary)?;
payload.set("shift", *shift)?;
}
- InputEvent::Change | InputEvent::Focus | InputEvent::Blur => {}
+ InputEvent::Change
+ | InputEvent::Focus
+ | InputEvent::Blur
+ | InputEvent::GutterMarkerMouseDown { .. } => {}
}
handler.call::<_, ()>((
payload,
diff --git a/crates/story/src/stories/input_story.rs b/crates/story/src/stories/input_story.rs
index 1c89b48787..2dc117c5bd 100644
--- a/crates/story/src/stories/input_story.rs
+++ b/crates/story/src/stories/input_story.rs
@@ -364,6 +364,7 @@ impl InputStory {
}
InputEvent::Focus => println!("Focus"),
InputEvent::Blur => println!("Blur"),
+ InputEvent::GutterMarkerMouseDown { .. } => {}
};
}
}
diff --git a/crates/story/src/stories/number_input_story.rs b/crates/story/src/stories/number_input_story.rs
index af70040566..04fcfa4954 100644
--- a/crates/story/src/stories/number_input_story.rs
+++ b/crates/story/src/stories/number_input_story.rs
@@ -155,6 +155,7 @@ impl NumberInputStory {
}
InputEvent::Focus => println!("Focus"),
InputEvent::Blur => println!("Blur"),
+ InputEvent::GutterMarkerMouseDown { .. } => {}
}
}