Files
Ely-GPUI-Components/src/documents/document/visual.rs
T
ZacharyZhang-NYandClaude Opus 5.5 2b6bd1d369
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m35s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Documents: untouched cells keep their bytes; open cells take padding presses
A trailing backslash doubles only before a closing pipe. A write at a
block's start keeps the block, so Tab leaves a first cell without outer
pipes. Padding of the open cell moves its caret to the end, and a right
press there points at the end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 01:39:17 -04:00

353 lines
14 KiB
Rust

use gpui::{
AnyElement, App, ClickEvent, Context, ExternalPaths, FontWeight, InteractiveElement,
IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement, Render,
StatefulInteractiveElement, Styled, StyledText, Window, actions, div, list,
prelude::FluentBuilder as _,
};
use super::{MarkdownDocument, edit::Landing};
use crate::{
documents::{
MarkdownMode,
render::{Hooks, OpenCell, Pointed, draw_nodes, point_lines},
},
forms::{self, Down, Up},
primitives::FocusRing as _,
theme::{ActiveTheme, Radius, TextSize},
};
actions!(markdown_document, [Undo, Redo, Indent, Outdent, Paste]);
const CONTEXT: &str = "MarkdownDocument";
/// How far a faded block recedes.
const FADED: f32 = 0.35;
/// Modes with a reader's block to keep.
fn editing_mode(mode: MarkdownMode) -> bool {
mode == MarkdownMode::Visual
}
/// 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("secondary-v", Paste, 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`, with typewriter room at the ends.
fn item(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let (first, last) = (ix == 0, ix == self.parsed.blocks.len());
let room = self.room;
div()
.when(first, |item| item.pt(room))
.when(last, |item| item.pb(room))
.child(self.block_item(ix, window, cx))
.into_any_element()
}
/// A block, or the end.
fn block_item(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let editing = self.mode == MarkdownMode::Visual;
let drawn = editing && ix < self.parsed.blocks.len();
let on_point = drawn.then(|| self.on_point(ix, cx));
let theme = cx.theme();
let colors = theme.colors.clone();
let (radius, body, mono) = (
theme.radius(Radius::Md),
theme.text_size(TextSize::Base) * self.look.scale,
theme.mono_family.clone(),
);
// Focus fades all but the reader's block.
let faded = self.look.focus && editing_mode(self.mode) && ix != self.current_block(cx);
let open = self.open.as_ref();
let point = Self::point_in_open(cx);
let field = |open: &super::Open| {
div()
.debug_selector(|| "markdown-open".to_string())
.on_mouse_down(MouseButton::Right, point)
.text_size(body)
.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.cell.is_none() && open.range == block.range) {
return field(open);
}
// A cell open in this block, typed in place.
let cell = open.and_then(|open| {
let inside = block.range.start <= open.range.start && open.range.end <= block.range.end;
Some(OpenCell {
at: open.cell.filter(|_| inside)?,
field: std::cell::RefCell::new(Some(
div()
.debug_selector(|| "markdown-open-cell".to_string())
.on_mouse_down(MouseButton::Right, Self::point_in_open(cx))
.rounded(radius)
.bg(colors.sunken)
.child(open.input.clone())
.into_any_element(),
)),
})
});
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,
Hooks {
base: &self.base,
on_link: &self.on_link,
on_heading: &self.on_heading,
on_point: &on_point,
cell: cell.as_ref(),
},
self.look.scale,
cx,
)),
None => {
let raw = &self.parsed.text[block.range.clone()];
let drawn = StyledText::new(raw.to_string());
let layout = drawn.layout().clone();
let shown = div()
.text_size(body)
.font_family(mono)
.text_color(colors.fg_muted)
.child(drawn)
.into_any_element();
div().child(point_lines(
shown,
layout,
raw,
0,
&on_point,
(false, false),
))
}
};
let block = div()
.id(("block", ix))
.debug_selector(move || format!("markdown-block-{ix}"))
.px_3()
.py_1()
.rounded(radius)
.when(faded, |block| block.opacity(FADED))
.child(shown);
match editing {
true => self.pressable(block, Some(ix), cx).into_any_element(),
false => block.into_any_element(),
}
}
/// A right press in the open field points there.
fn point_in_open(
cx: &mut Context<Self>,
) -> impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static {
cx.listener(|document, event: &MouseDownEvent, window, cx| {
window.prevent_default();
cx.stop_propagation();
document.point_open(Some(event.position), cx);
})
}
/// `element` opens block `ix`; a right press points.
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, Landing::End, window, cx);
}),
)
.on_mouse_down(
MouseButton::Right,
cx.listener(move |document, _: &MouseDownEvent, window, cx| {
window.prevent_default();
cx.stop_propagation();
document.point(ix, None, Pointed::block(), window, cx);
}),
)
.on_click(
cx.listener(move |document, event: &ClickEvent, window, cx| {
if event.is_keyboard() {
document.open(ix, Landing::End, window, cx);
}
}),
)
}
}
impl Render for MarkdownDocument {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.take_orphan(window, cx);
// A top line asked before its text was read.
self.place_top(window, cx);
self.show_waiting(window, cx);
let colors = cx.theme().colors.clone();
let root = div()
.size_full()
.bg(colors.surface)
.font_weight(FontWeight::NORMAL)
.on_drop(cx.listener(|_, dropped: &ExternalPaths, _, cx| {
log::info!("markdown document: {} files dropped", dropped.paths().len());
cx.emit(super::Attachment::Files(dropped.paths().to_vec()));
}));
self.center_current(cx);
// Typewriter: half a view past each end.
self.make_room();
let blocks = list(self.list.clone(), cx.processor(Self::item)).size_full();
// The text column: its measure, font, centered.
let column = |content: gpui::AnyElement, look: &super::DocumentLook| {
div().size_full().flex().justify_center().child(
div()
.h_full()
.w_full()
.when_some(look.measure, |column, measure| column.max_w(measure))
.when_some(look.font.clone(), |column, font| column.font_family(font))
.child(content),
)
};
let blocks = column(blocks.into_any_element(), &self.look);
let source = column(self.editor.clone().into_any_element(), &self.look);
match self.mode {
MarkdownMode::Source => root
.capture_action(cx.listener(|document, _: &crate::editor::Paste, _, cx| {
if document.paste_picture(cx) {
cx.stop_propagation();
}
}))
.capture_action(cx.listener(|document, _: &crate::editor::Newline, _, cx| {
if document.source_newline(cx) {
cx.stop_propagation();
}
}))
.child(source)
.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::Paste, _, cx| {
if document.paste_picture(cx) {
cx.stop_propagation();
}
}))
.on_action(cx.listener(|document, _: &Paste, _, cx| {
if document.paste_picture(cx) {
return;
}
match cx.read_from_clipboard().and_then(|item| item.text()) {
Some(text) => _ = document.insert(&text, cx),
None => log::info!("markdown document: nothing to paste"),
}
}))
.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(),
}
}
}