Files
Ely-GPUI-Components/src/documents/document/visual.rs
T
ZacharyZhang-NYandClaude Opus 5.5 ce4a1e9cb5
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 11m41s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
documents: lists go on, block lines shift, tables change, matches show, fences color by language
The document carries Markdown's editing tools for ElyMark (MARK-06):
Enter continues a list item or ends an empty one, Tab and Shift-Tab
shift an open block's lines, table rows and columns come and go as
one undo step, a match shows in any mode, replacements are one step,
and a fenced block colors by its language.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:14:34 -04:00

223 lines
8.3 KiB
Rust

use gpui::{
AnyElement, App, ClickEvent, Context, FontWeight, InteractiveElement, IntoElement, KeyBinding,
MouseButton, MouseDownEvent, ParentElement, Render, StatefulInteractiveElement, Styled, Window,
actions, div, list,
};
use super::MarkdownDocument;
use crate::{
documents::{MarkdownMode, render::draw_nodes},
forms::{self, Down, Up},
primitives::FocusRing as _,
theme::{ActiveTheme, Radius, TextSize},
};
actions!(markdown_document, [Undo, Redo, Indent, Outdent]);
const CONTEXT: &str = "MarkdownDocument";
/// Undo and redo; Tab shifts an open block's lines.
pub(crate) fn bind_keys(cx: &mut App) {
let context = Some(CONTEXT);
let block = Some("MarkdownDocument > ElyInput");
let mut bindings = vec![
KeyBinding::new("secondary-z", Undo, context),
KeyBinding::new("secondary-shift-z", Redo, context),
KeyBinding::new("tab", Indent, block),
KeyBinding::new("shift-tab", Outdent, block),
];
if !cfg!(target_os = "macos") {
bindings.push(KeyBinding::new("ctrl-y", Redo, context));
}
cx.bind_keys(bindings);
}
impl MarkdownDocument {
/// List item `ix`: a block, or the end.
fn item(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let (radius, body, mono) = (
theme.radius(Radius::Md),
theme.text_size(TextSize::Base),
theme.mono_family.clone(),
);
let editing = self.mode == MarkdownMode::Visual;
let open = self.open.as_ref();
let field = |open: &super::Open| {
div()
.px_3()
.py_2()
.rounded(radius)
.bg(colors.sunken)
.child(open.input.clone())
.into_any_element()
};
let Some(block) = self.parsed.blocks.get(ix) else {
return match (open, editing) {
(Some(open), _) if open.append => field(open),
(_, true) => self
.pressable(div().id("more").min_h_8(), None, cx)
.into_any_element(),
(_, false) => div().into_any_element(),
};
};
if let Some(open) = open.filter(|open| open.range == block.range) {
return field(open);
}
let shown = match &block.nodes {
Some(nodes) => div()
.flex()
.flex_col()
.gap_3()
.text_size(body)
.line_height(body * 1.6)
.text_color(colors.fg)
.children(draw_nodes(
&("render", ix).into(),
nodes,
&self.base,
&self.on_link,
cx,
)),
None => div()
.font_family(mono)
.text_color(colors.fg_muted)
.child(self.parsed.text[block.range.clone()].to_string()),
};
let block = div()
.id(("block", ix))
.debug_selector(move || format!("markdown-block-{ix}"))
.px_3()
.py_1()
.rounded(radius)
.child(shown);
match editing {
true => self.pressable(block, Some(ix), cx).into_any_element(),
false => block.into_any_element(),
}
}
/// `element` opens block `ix` on a press, Enter or Space.
fn pressable(
&self,
element: gpui::Stateful<gpui::Div>,
ix: Option<usize>,
cx: &mut Context<Self>,
) -> gpui::Stateful<gpui::Div> {
let hover = cx.theme().colors.hover.opacity(0.5);
element
.tab_index(0)
.border_1()
.border_color(gpui::transparent_black())
.focus_ring(cx)
.cursor_text()
.hover(move |element| element.bg(hover))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |document, event: &MouseDownEvent, window, cx| {
// Cmd or Ctrl leaves the press to a link.
if event.modifiers.secondary() {
return;
}
window.prevent_default();
cx.stop_propagation();
document.open(ix, false, window, cx);
}),
)
.on_click(
cx.listener(move |document, event: &ClickEvent, window, cx| {
if event.is_keyboard() {
document.open(ix, false, window, cx);
}
}),
)
}
}
impl Render for MarkdownDocument {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// A top line asked before its text was read.
self.place_top(window, cx);
let colors = cx.theme().colors.clone();
let root = div()
.size_full()
.bg(colors.surface)
.font_weight(FontWeight::NORMAL);
let blocks = list(self.list.clone(), cx.processor(Self::item)).size_full();
match self.mode {
MarkdownMode::Source => root
.capture_action(cx.listener(|document, _: &crate::editor::Newline, _, cx| {
if document.source_newline(cx) {
cx.stop_propagation();
}
}))
.child(self.editor.clone())
.into_any_element(),
MarkdownMode::Split => root
.flex()
.child(div().flex_1().min_w_0().h_full().child(self.editor.clone()))
.child(div().flex_1().min_w_0().h_full().child(blocks))
.into_any_element(),
MarkdownMode::Preview => root
.track_focus(&self.focus)
.px_2()
.py_3()
.child(blocks)
.into_any_element(),
MarkdownMode::Visual => root
.key_context(CONTEXT)
.track_focus(&self.focus)
.on_action(cx.listener(|document, _: &Undo, window, cx| {
document.history(false, window, cx);
}))
.on_action(cx.listener(|document, _: &Redo, window, cx| {
document.history(true, window, cx);
}))
// One history: a block's own never runs.
.capture_action(cx.listener(|document, _: &forms::Undo, window, cx| {
cx.stop_propagation();
document.history(false, window, cx);
}))
.capture_action(cx.listener(|document, _: &forms::Redo, window, cx| {
cx.stop_propagation();
document.history(true, window, cx);
}))
.capture_action(cx.listener(|document, _: &forms::Enter, _, cx| {
if document.block_newline(cx) {
cx.stop_propagation();
}
}))
.on_action(cx.listener(|document, _: &Indent, window, cx| {
document.block_shift(true, window, cx);
}))
.on_action(cx.listener(|document, _: &Outdent, window, cx| {
document.block_shift(false, window, cx);
}))
.capture_action(cx.listener(|document, _: &Up, window, cx| {
if document.step(false, window, cx) {
cx.stop_propagation();
}
}))
.capture_action(cx.listener(|document, _: &Down, window, cx| {
if document.step(true, window, cx) {
cx.stop_propagation();
}
}))
.on_key_down(
cx.listener(|document, event: &gpui::KeyDownEvent, window, cx| {
if event.keystroke.key == "escape" && document.open.is_some() {
cx.stop_propagation();
document.commit(cx);
window.focus(&document.focus, cx);
}
}),
)
.px_2()
.py_3()
.child(blocks)
.into_any_element(),
}
}
}