gpui and gpui_platform now come from Zed's repo by git at 1a28cff, the code gpui-pre 0.3.7 republishes, so the gallery can later build for the browser. gpui takes stacker, as 0.2.2 always did, and gpui_platform takes font-kit, without which macOS draws no text; runtime_shaders moves to gpui_platform. The API moved: focus, blur and Tab steps take cx, Corner is Anchor, shaped lines paint with an alignment, defer_draw takes a content mask, the inspector registers a factory, async updates return their value, and wrapped lines are shared, not cloned. Updates through a window context go through the window, so a closed window still ends them as it did. Where the new gpui behaves differently, Ely keeps its old intent. Holds and reveals under a resting pointer survive typing through gpui's input-modality-independent hover; previews the keyboard also moves keep the default, and the calendar's leave ignores a key press. Enter reaches a button inside the code editor, since gpui now arms a key click on the press. The event logger skips a lone modifier. A video and an image grid keep their shape in a flex column under taffy 0.13, as a picture does. AGENTS.md records the new stack and the rules this port changes; the rest of the old gpui and taffy rules are re-tested in T44c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
410 lines
14 KiB
Rust
410 lines
14 KiB
Rust
use std::ops::Range;
|
|
|
|
use gpui::{
|
|
AnyElement, Context, FontWeight, HighlightStyle, InteractiveElement, IntoElement, MouseButton,
|
|
MouseDownEvent, MouseMoveEvent, ParentElement, Stateful, Styled, StyledText, UnderlineStyle,
|
|
Window, canvas, div, prelude::*,
|
|
};
|
|
|
|
use super::{
|
|
cursor::Selection,
|
|
gutter::Cell,
|
|
layout::{Shown, stack},
|
|
state::{CodeEditor, CursorShape},
|
|
syntax,
|
|
};
|
|
use crate::{
|
|
charts::tint,
|
|
theme::{ActiveTheme, Radius},
|
|
};
|
|
|
|
impl CodeEditor {
|
|
/// A line's own styles: syntax colors, rainbow brackets, problems, washes, matches and the matched pair.
|
|
fn styles(&self, line: usize, cx: &Context<Self>) -> Vec<(Range<usize>, HighlightStyle)> {
|
|
let colors = &cx.theme().colors;
|
|
let range = self.row_range(line);
|
|
let local = |span: &Range<usize>| {
|
|
span.start.max(range.start) - range.start..span.end.min(range.end) - range.start
|
|
};
|
|
let touches = |span: &Range<usize>| {
|
|
span.start < range.end.max(range.start + 1) && range.start < span.end
|
|
};
|
|
let mut spans = syntax::colors(self.row_text(line), cx);
|
|
let brackets = self
|
|
.frame
|
|
.brackets
|
|
.iter()
|
|
.filter(|bracket| range.contains(&bracket.offset));
|
|
for bracket in brackets {
|
|
let at = bracket.offset - range.start;
|
|
let color = if self.options.rainbow {
|
|
tint(colors, bracket.depth % 3)
|
|
} else {
|
|
colors.syntax.punctuation
|
|
};
|
|
spans.retain(|(span, _)| *span != (at..at + 1));
|
|
spans.push((at..at + 1, color.into()));
|
|
}
|
|
for span in self.frame.occurrences.iter().filter(|span| touches(span)) {
|
|
spans.push((
|
|
local(span),
|
|
HighlightStyle {
|
|
background_color: Some(colors.selection.opacity(0.45)),
|
|
..HighlightStyle::default()
|
|
},
|
|
));
|
|
}
|
|
for (span, color) in self
|
|
.marks
|
|
.backgrounds
|
|
.iter()
|
|
.filter(|(span, _)| touches(span))
|
|
{
|
|
spans.push((
|
|
local(span),
|
|
HighlightStyle {
|
|
background_color: Some(*color),
|
|
..HighlightStyle::default()
|
|
},
|
|
));
|
|
}
|
|
for diagnostic in self
|
|
.marks
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|diagnostic| touches(&diagnostic.range))
|
|
{
|
|
spans.push((
|
|
local(&diagnostic.range),
|
|
HighlightStyle {
|
|
underline: Some(UnderlineStyle {
|
|
thickness: cx.theme().underline_thickness(),
|
|
color: Some(diagnostic.severity.color(colors)),
|
|
wavy: true,
|
|
}),
|
|
..HighlightStyle::default()
|
|
},
|
|
));
|
|
}
|
|
if let Some((open, close)) = self.frame.matched {
|
|
for at in [open, close].into_iter().filter(|at| range.contains(at)) {
|
|
let at = at - range.start;
|
|
spans.push((
|
|
at..at + 1,
|
|
HighlightStyle {
|
|
background_color: Some(colors.active),
|
|
font_weight: Some(FontWeight::BOLD),
|
|
..HighlightStyle::default()
|
|
},
|
|
));
|
|
}
|
|
}
|
|
stack(spans)
|
|
}
|
|
|
|
/// The columns guides stand at on a line: every tab stop inside its indent, or the next line's when it is blank.
|
|
fn guides(&self, line: usize) -> Vec<usize> {
|
|
let indent = (line..self.buffer.lines())
|
|
.find(|at| !self.buffer.line(*at).trim().is_empty())
|
|
.map_or(0, |at| self.buffer.indent(at));
|
|
(0..indent).step_by(self.options.tab).collect()
|
|
}
|
|
|
|
/// Dots where the line has spaces and arrows for tabs, spaces elsewhere, so they fall on the text's columns.
|
|
fn whitespace(&self, line: usize, shown: &Shown, cx: &Context<Self>) -> String {
|
|
let text = self.row_text(line);
|
|
let notes = self.notes(line, cx);
|
|
let mut out = String::new();
|
|
let mut note = notes.iter().peekable();
|
|
for (at, ch) in text.char_indices() {
|
|
while let Some((_, words, _)) = note.next_if(|(place, ..)| *place <= at) {
|
|
out.extend(std::iter::repeat_n(' ', words.chars().count()));
|
|
}
|
|
out.push(match ch {
|
|
' ' => '·',
|
|
'\t' => '→',
|
|
_ => ' ',
|
|
});
|
|
}
|
|
debug_assert!(out.chars().count() <= shown.text.chars().count());
|
|
out
|
|
}
|
|
|
|
pub(crate) fn line_row(
|
|
&self,
|
|
base: Stateful<gpui::Div>,
|
|
row: usize,
|
|
line: usize,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let colors = theme.colors.clone();
|
|
let (advance, height) = (self.frame.advance, self.frame.line);
|
|
let text = self.row_text(line);
|
|
let range = self.row_range(line);
|
|
let shown = Shown::new(text, self.notes(line, cx), self.styles(line, cx));
|
|
let x = |column: usize| advance * (column + 1) as f32;
|
|
let focused = self.focus.is_focused(window);
|
|
let primary = self.primary();
|
|
let current = primary.is_empty() && self.buffer.line_of(primary.head) == line;
|
|
let column_of =
|
|
|offset: usize| shown.column(text, self.buffer.point(offset).1.min(text.len()));
|
|
let selections = self.selections.iter().filter_map(|selection| {
|
|
let span = selection.range();
|
|
if span.is_empty() || span.end < range.start || span.start > range.end {
|
|
return None;
|
|
}
|
|
let start = if span.start <= range.start {
|
|
0
|
|
} else {
|
|
column_of(span.start)
|
|
};
|
|
let past = span.end > range.end;
|
|
let end = if past {
|
|
column_of(range.end) + 1
|
|
} else {
|
|
column_of(span.end)
|
|
};
|
|
(end > start).then(|| {
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.left(x(start))
|
|
.w(advance * (end - start) as f32)
|
|
.h(height)
|
|
.bg(if focused {
|
|
colors.selection
|
|
} else {
|
|
colors.selection.opacity(0.5)
|
|
})
|
|
})
|
|
});
|
|
let active = self
|
|
.frame
|
|
.folds
|
|
.iter()
|
|
.filter(|(header, end)| {
|
|
let at = self.buffer.line_of(primary.head);
|
|
*header < at && at <= *end
|
|
})
|
|
.map(|(header, _)| self.buffer.indent(*header))
|
|
.next_back();
|
|
let guides = if self.options.guides {
|
|
self.guides(line)
|
|
} else {
|
|
Default::default()
|
|
};
|
|
let rulers = self.options.rulers.iter().map(|column| {
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.left(x(*column))
|
|
.h(height)
|
|
.w_0()
|
|
.border_l_1()
|
|
.border_color(colors.border.opacity(0.6))
|
|
});
|
|
let guides = guides.into_iter().map(|column| {
|
|
let strong = active == Some(column);
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.left(x(column))
|
|
.h(height)
|
|
.w_0()
|
|
.border_l_1()
|
|
.border_color(if strong {
|
|
colors.border_strong
|
|
} else {
|
|
colors.border.opacity(0.6)
|
|
})
|
|
});
|
|
let carets = self
|
|
.selections
|
|
.iter()
|
|
.filter(|selection| self.buffer.line_of(selection.head) == line)
|
|
.filter(|_| focused && self.caret_on)
|
|
.map(|selection| {
|
|
let left = x(column_of(selection.head));
|
|
let caret = theme.caret_width().to_pixels(window.rem_size());
|
|
let mark = div().absolute().left(left);
|
|
match self.options.cursor {
|
|
CursorShape::Line => mark.top_0().w(caret).h(height).bg(colors.focus),
|
|
CursorShape::Block => mark
|
|
.top_0()
|
|
.w(advance)
|
|
.h(height)
|
|
.bg(colors.focus.opacity(0.35)),
|
|
CursorShape::Underline => mark
|
|
.top(height - caret * 1.5)
|
|
.w(advance)
|
|
.h(caret * 1.5)
|
|
.bg(colors.focus),
|
|
}
|
|
});
|
|
let dots = self.options.whitespace.then(|| {
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.left(x(0))
|
|
.h(height)
|
|
.line_height(height)
|
|
.whitespace_nowrap()
|
|
.text_color(colors.fg_subtle.opacity(0.6))
|
|
.child(self.whitespace(line, &shown, cx))
|
|
});
|
|
let folded = self.folded.contains(&line).then(|| {
|
|
let entity = cx.entity();
|
|
div()
|
|
.id(("unfold", line))
|
|
.absolute()
|
|
.top_0()
|
|
.left(x(shown.text.chars().count() + 1))
|
|
.h(height)
|
|
.flex()
|
|
.items_center()
|
|
.child(
|
|
div()
|
|
.px_1()
|
|
.rounded(theme.radius(Radius::Sm))
|
|
.bg(colors.hover)
|
|
.text_color(colors.fg_muted)
|
|
.line_height(height * 0.8)
|
|
.child("⋯"),
|
|
)
|
|
.cursor_pointer()
|
|
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
|
window.prevent_default();
|
|
entity.update(cx, |editor, cx| editor.toggle_fold(line, cx));
|
|
})
|
|
});
|
|
let added = self
|
|
.marks
|
|
.hunks
|
|
.iter()
|
|
.any(|hunk| (hunk.line..hunk.line + hunk.added).contains(&line));
|
|
let entity = cx.entity();
|
|
let left = x(0);
|
|
let code = div()
|
|
.id(("code", row))
|
|
.relative()
|
|
.flex_1()
|
|
.h_full()
|
|
.overflow_hidden()
|
|
.when(current && focused, |code| code.bg(colors.hover))
|
|
.when(added, |code| code.bg(colors.success.opacity(0.08)))
|
|
.children(selections)
|
|
.children(guides)
|
|
.children(rulers)
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.left(left)
|
|
.h(height)
|
|
.line_height(height)
|
|
.whitespace_nowrap()
|
|
.text_color(colors.syntax.variable)
|
|
.child(
|
|
StyledText::new(shown.text.clone()).with_highlights(shown.styles.clone()),
|
|
),
|
|
)
|
|
.children(dots)
|
|
.children(folded)
|
|
.children(carets)
|
|
.child(
|
|
canvas(
|
|
move |bounds, _, cx| {
|
|
let origin = bounds.origin.x + left;
|
|
entity.update(cx, |editor, _| {
|
|
if editor.metrics.left != origin {
|
|
editor.metrics.left = origin;
|
|
}
|
|
});
|
|
},
|
|
|_, _, _, _| {},
|
|
)
|
|
.absolute()
|
|
.top_0()
|
|
.left_0()
|
|
.size_full(),
|
|
)
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
cx.listener(move |editor, event: &MouseDownEvent, window, cx| {
|
|
editor.press(line, event, window, cx)
|
|
}),
|
|
)
|
|
.on_mouse_move(cx.listener(move |editor, event: &MouseMoveEvent, _, cx| {
|
|
editor.drag(line, event, cx)
|
|
}));
|
|
base.child(self.gutter(Cell::Line(line), row, cx))
|
|
.child(code)
|
|
.into_any_element()
|
|
}
|
|
|
|
/// A press on a line: one caret, a word on a double press, the line on a triple; Shift extends, Alt adds a caret.
|
|
pub(crate) fn press(
|
|
&mut self,
|
|
line: usize,
|
|
event: &MouseDownEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
window.focus(&self.focus, cx);
|
|
let at = self.offset_in(line, event.position.x, cx);
|
|
let chosen = match event.click_count {
|
|
2 => {
|
|
let word = self.buffer.word_at(at);
|
|
vec![Selection {
|
|
anchor: word.start,
|
|
head: word.end,
|
|
goal: None,
|
|
}]
|
|
}
|
|
3.. => {
|
|
let range = self.buffer.line_range(line);
|
|
vec![Selection {
|
|
anchor: range.start,
|
|
head: self.buffer.next(range.end),
|
|
goal: None,
|
|
}]
|
|
}
|
|
_ if event.modifiers.shift => {
|
|
let mut all = self.selections.clone();
|
|
let primary = all.last_mut().expect("an editor keeps a cursor");
|
|
primary.head = at;
|
|
primary.goal = None;
|
|
all
|
|
}
|
|
_ if event.modifiers.alt => {
|
|
let mut all = self.selections.clone();
|
|
all.push(Selection::caret(at));
|
|
all
|
|
}
|
|
_ => {
|
|
self.dragging = true;
|
|
vec![Selection::caret(at)]
|
|
}
|
|
};
|
|
self.set_selections(chosen, cx);
|
|
}
|
|
|
|
/// A drag over a line stretches the primary selection to where the pointer is.
|
|
pub(crate) fn drag(&mut self, line: usize, event: &MouseMoveEvent, cx: &mut Context<Self>) {
|
|
if !self.dragging || !event.dragging() {
|
|
return;
|
|
}
|
|
let at = self.offset_in(line, event.position.x, cx);
|
|
let mut all = self.selections.clone();
|
|
let primary = all.last_mut().expect("an editor keeps a cursor");
|
|
if primary.head == at {
|
|
return;
|
|
}
|
|
primary.head = at;
|
|
primary.goal = None;
|
|
self.set_selections(all, cx);
|
|
}
|
|
}
|