From 6a9ce8a2c36ad80ca257d8cdf1f51d7dbb52a1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 26 Sep 2026 03:32:24 -0400 Subject: [PATCH] T21a: Documents, editing - markdown read with pulldown-cmark: marks for live styling, blocks, links, words - forms::Highlight gains weight, slant and strike; measure and paint share runs - Inter Italic embedded, so emphasis reads slanted - forms::mention's popup and keys become Suggestions, shared with the editors - RichTextEditor with FixedFormatToolbar, the floating toolbar, LinkEditor, @ and : suggestions, WordCount and ReadingTime - MarkdownEditor: visual (a pressed block opens as source), source, split - BlockEditor: prose, lists, to-dos, quotes, callouts, toggles, code, math, diagrams the app draws, dividers, tables that grow and merge, pictures to resize and align, cards for video and pages, columns, synced text; slash menu, markdown shortcuts, handles that drag and open a menu, one undo for all - TodoItem and Checklist - icons: heading-1..3, workflow, merge - gallery: the Documents page with scripted states --- AGENTS.md | 6 +- Cargo.lock | 12 + Cargo.toml | 1 + TASKS.md | 3 +- assets/fonts/Inter-Italic.ttf | Bin 0 -> 417388 bytes assets/icons/heading-1.svg | 18 + assets/icons/heading-2.svg | 18 + assets/icons/heading-3.svg | 19 + assets/icons/merge.svg | 17 + assets/icons/workflow.svg | 17 + examples/gallery/pages/documents/editing.rs | 234 ++++++++++ examples/gallery/pages/documents/mod.rs | 65 +++ examples/gallery/pages/mod.rs | 2 + src/documents/blocks/content.rs | 235 ++++++++++ src/documents/blocks/edit.rs | 115 +++++ src/documents/blocks/keys.rs | 144 ++++++ src/documents/blocks/kind.rs | 277 +++++++++++ src/documents/blocks/media.rs | 371 +++++++++++++++ src/documents/blocks/mod.rs | 479 ++++++++++++++++++++ src/documents/blocks/table.rs | 287 ++++++++++++ src/documents/blocks/view.rs | 288 ++++++++++++ src/documents/counts.rs | 85 ++++ src/documents/format.rs | 233 ++++++++++ src/documents/link.rs | 233 ++++++++++ src/documents/markdown.rs | 279 ++++++++++++ src/documents/mod.rs | 32 ++ src/documents/modes.rs | 467 +++++++++++++++++++ src/documents/render.rs | 357 +++++++++++++++ src/documents/rich.rs | 186 ++++++++ src/documents/suggest.rs | 104 +++++ src/documents/tests.rs | 184 ++++++++ src/documents/todo.rs | 146 ++++++ src/documents/toolbar.rs | 121 +++++ src/documents/tree.rs | 319 +++++++++++++ src/forms/code.rs | 8 +- src/forms/expression.rs | 8 +- src/forms/glyphs.rs | 2 +- src/forms/mention.rs | 204 +++++---- src/forms/mod.rs | 6 +- src/forms/pattern.rs | 8 +- src/forms/text/element.rs | 154 +++++-- src/forms/text/highlight.rs | 24 + src/forms/text/mod.rs | 13 +- src/lib.rs | 2 + src/primitives/icon.rs | 5 + tasks/ch21-30.md | 40 +- 46 files changed, 5635 insertions(+), 193 deletions(-) create mode 100644 assets/fonts/Inter-Italic.ttf create mode 100644 assets/icons/heading-1.svg create mode 100644 assets/icons/heading-2.svg create mode 100644 assets/icons/heading-3.svg create mode 100644 assets/icons/merge.svg create mode 100644 assets/icons/workflow.svg create mode 100644 examples/gallery/pages/documents/editing.rs create mode 100644 examples/gallery/pages/documents/mod.rs create mode 100644 src/documents/blocks/content.rs create mode 100644 src/documents/blocks/edit.rs create mode 100644 src/documents/blocks/keys.rs create mode 100644 src/documents/blocks/kind.rs create mode 100644 src/documents/blocks/media.rs create mode 100644 src/documents/blocks/mod.rs create mode 100644 src/documents/blocks/table.rs create mode 100644 src/documents/blocks/view.rs create mode 100644 src/documents/counts.rs create mode 100644 src/documents/format.rs create mode 100644 src/documents/link.rs create mode 100644 src/documents/markdown.rs create mode 100644 src/documents/mod.rs create mode 100644 src/documents/modes.rs create mode 100644 src/documents/render.rs create mode 100644 src/documents/rich.rs create mode 100644 src/documents/suggest.rs create mode 100644 src/documents/tests.rs create mode 100644 src/documents/todo.rs create mode 100644 src/documents/toolbar.rs create mode 100644 src/documents/tree.rs create mode 100644 src/forms/text/highlight.rs diff --git a/AGENTS.md b/AGENTS.md index 461fcf9..d30b127 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,12 +6,13 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - Rust 1.95, edition 2024. One crate: `ely-gpui-component`. - gpui 0.2.2 from crates.io. Feature `runtime_shaders` is on by default: machines without Xcode lack the Metal compiler. -- Assets, embedded with `rust-embed`: Lucide 1.48.0 icons (ISC), Inter 4.1 and JetBrains Mono 2.304 (OFL). +- Assets, embedded with `rust-embed`: Lucide 1.48.0 icons (ISC), Inter 4.1 (regular, medium, semibold, italic) and JetBrains Mono 2.304 (OFL). - Catalog data: `emojis` (Unicode emoji), `isolang` (ISO 639, native names), `isocountry` (ISO 3166), `iso_currency` (ISO 4217, also minor units for money). - Codes: `qrcode` (QR) and `barcoders` (Code 128, EAN-13), both MIT OR Apache-2.0, default features off. - macOS extras (tray icon, Dock badge) call AppKit through `cocoa` 0.26 and `objc` 0.2, the crates gpui already links. - Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off) for the grid, its parser and the pseudo-terminal; `futures` carries its events. - Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation. +- Markdown: `pulldown-cmark` 0.13 (MIT, default features off) for CommonMark with tables, tasks, strikethrough, footnotes and math. - Gallery: `examples/gallery`. Website: `frontend/` (Vite 8, pnpm), built after the components. ## Commands @@ -110,6 +111,9 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - A label pinned to a point sits in a zero-size absolute box that centers it with flex (`charts::axes`), so no offset guesses at text size. - Tokens live in `src/theme`; the raw `px()` scan skips that folder only. - A callback field holds a named `type` alias; clippy's type_complexity rejects `Option>`. A chapter shares one alias per shape. +- A component that fills its container says `w_full` on its root. As a window's root, or in a flex row, it would shrink to its text's narrowest width. +- A field's `Highlight` sets color, wash, weight, slant and strike per span; measuring and painting share one set of runs, so wrapped heights match. +- A block editor keeps one undo for the document: text and structure together, as snapshots through `forms::History`; it takes Undo and Redo before its fields do. - A terminal takes Tab and Shift-Tab through bindings under `ElyTerminal`, ahead of focus moves; other keys go through `terminal::keys` in a key listener, and typed text through its input handler. - A shell in the gallery starts without startup files and with a fixed prompt, so captures show no user or host. - gpui 0.2.2 has no `(ElementId, usize)` id. A child id names its index: `(self.id.clone(), format!("row-{ix}"))`. diff --git a/Cargo.lock b/Cargo.lock index b8b9883..9d66f08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1597,6 +1597,7 @@ dependencies = [ "jiff", "log", "objc", + "pulldown-cmark", "qrcode", "raw-window-handle", "regex", @@ -4345,6 +4346,17 @@ dependencies = [ "cc", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.2", + "memchr", + "unicase", +] + [[package]] name = "pulp" version = "0.22.3" diff --git a/Cargo.toml b/Cargo.toml index 37acfac..09a0eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ isocountry = "0.3" isolang = { version = "2.4", features = ["local_names", "list_languages"] } jiff = "0.2" log = "0.4" +pulldown-cmark = { version = "0.13", default-features = false } qrcode = { version = "0.14", default-features = false } regex = "1" rust-embed = "8.12" diff --git a/TASKS.md b/TASKS.md index 801a967..7b02081 100644 --- a/TASKS.md +++ b/TASKS.md @@ -68,7 +68,7 @@ Per-component lines live in `tasks/`. Tags there: - [x] T18 Terminal - [x] T19 Git - [x] T20 Debug -- [ ] T21a Documents · Editing — `tasks/ch21-30.md` +- [x] T21a Documents · Editing — `tasks/ch21-30.md` - [ ] T21b Documents · Reading - [ ] T21c Documents · Knowledge - [ ] T22 Collaboration @@ -247,3 +247,4 @@ Per-component lines live in `tasks/`. Tags there: | T19 | 3 | PASS | | | T20 | 1 | FAIL | the demo picked past memory's end; an empty gutter had no height to press; a breakpoint's box also opened it; zoomed ticks lost their places; docs promised bound keys and batch actions | | T20 | 2 | PASS | | +| T21a | 1 | — | pending review | diff --git a/assets/fonts/Inter-Italic.ttf b/assets/fonts/Inter-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7ed211d2d6052b781509fe7bf253951eac7162f GIT binary patch literal 417388 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Ç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 literal 0 HcmV?d00001 diff --git a/assets/icons/heading-1.svg b/assets/icons/heading-1.svg new file mode 100644 index 0000000..f96e6a4 --- /dev/null +++ b/assets/icons/heading-1.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/heading-2.svg b/assets/icons/heading-2.svg new file mode 100644 index 0000000..4c4cfec --- /dev/null +++ b/assets/icons/heading-2.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/heading-3.svg b/assets/icons/heading-3.svg new file mode 100644 index 0000000..53539bd --- /dev/null +++ b/assets/icons/heading-3.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/merge.svg b/assets/icons/merge.svg new file mode 100644 index 0000000..e6b9f17 --- /dev/null +++ b/assets/icons/merge.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/workflow.svg b/assets/icons/workflow.svg new file mode 100644 index 0000000..368df4e --- /dev/null +++ b/assets/icons/workflow.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/examples/gallery/pages/documents/editing.rs b/examples/gallery/pages/documents/editing.rs new file mode 100644 index 0000000..bc86a0e --- /dev/null +++ b/examples/gallery/pages/documents/editing.rs @@ -0,0 +1,234 @@ +use ely_gpui_component::{ + documents::{ + BlockData, BlockEditor, BlockKind, Checklist, MarkdownEditor, MarkdownMode, Media, + RichTextEditor, markdown_highlights, + }, + forms::TextInput, +}; +use gpui::{App, Entity, IntoElement, ParentElement, Styled, Window, div, px}; + +use crate::{ + probe::probe, + ui::{keep, section, set}, +}; + +const ATRIUM: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/examples/gallery/assets/atrium.jpg" +); + +const PEOPLE: [&str; 4] = ["ada", "grace", "linus", "margaret"]; + +const NOTES: &str = "## Release notes\n\nBlend every accent **toward white** by *lift*, clamped in gamma space. See [the palette docs](https://ely.dev/palette) or ask @ada.\n\n- Tokens stay in `src/theme`\n- ~~Raw pixels~~ never\n- [x] Light and dark"; + +const ESSAY: &str = "# Lift\n\nA lift blends a color toward white. **Half** a lift sits midway, and a *full* one is white.\n\n> Tokens live in one place; components only read them.\n\n| Token | Light | Dark |\n|:--|--:|--:|\n| accent | 0.50 | 0.62 |\n| focus | 0.40 | 0.55 |\n\n```rust\nfn lift(color: Color, by: f32) -> Color {\n color.mix(WHITE, by)\n}\n```\n\n$$c' = c + (1 - c)\\,t$$"; + +/// A markdown field of several lines, styled as it is typed. +fn markdown( + key: &'static str, + text: &'static str, + rows: usize, + window: &mut Window, + cx: &mut App, +) -> Entity { + window.use_keyed_state(key, cx, move |window, cx| { + let mut field = TextInput::new(window, cx) + .multi_line(rows, rows * 3) + .highlighter(markdown_highlights); + field.set_text(text, cx); + field + }) +} + +/// Draws `graph LR; a --> b --> c` as boxes in a row, as a host's renderer would. +fn draw(source: &str) -> Result { + let (head, body) = source + .split_once(';') + .ok_or("start with `graph LR;` then name the steps")?; + if head.trim() != "graph LR" { + return Err(format!("only `graph LR` draws here, not `{}`", head.trim())); + } + let nodes: Vec<&str> = body.split("-->").map(str::trim).collect(); + if nodes.iter().any(|node| node.is_empty()) { + return Err("each arrow needs a step on both sides".into()); + } + let (width, gap, height) = (120, 48, 44); + let total = nodes.len() * width + (nodes.len() - 1) * gap; + let mut svg = format!( + "" + ); + for (ix, node) in nodes.iter().enumerate() { + let x = ix * (width + gap); + svg.push_str(&format!("{node}", height - 2, x + width / 2)); + if ix + 1 < nodes.len() { + let from = x + width; + svg.push_str(&format!("", from + 4, from + gap - 4, from + gap - 10, from + gap - 4, from + gap - 10)); + } + } + svg.push_str(""); + Ok(svg) +} + +fn block(kind: BlockKind, texts: &[&str]) -> BlockData { + BlockData::new(kind, texts.iter().copied()) +} + +fn notes() -> Vec { + vec![ + block(BlockKind::Heading(1), &["Project notes"]), + block( + BlockKind::Paragraph, + &[ + "Type **/** on an empty line for every kind of block; `#`, `-`, `[]` and `>` at a line's start change it as you type.", + ], + ), + block(BlockKind::Todo(true), &["Draft the palette"]), + block(BlockKind::Todo(false), &["Check contrast in dark"]), + block( + BlockKind::Callout, + &["Blocks drag by their handle; its menu turns, copies or deletes one."], + ), + block( + BlockKind::Toggle(true), + &[ + "Why gamma?", + "Blending in linear light washes out the midtones; gamma keeps them.", + ], + ), + ] +} + +fn kinds() -> Vec { + vec![ + block( + BlockKind::Code, + &["fn lift(color: Color, by: f32) -> Color {\n color.mix(WHITE, by)\n}"], + ), + block(BlockKind::Math, &["c' = c + (1 - c)\\,t"]), + block( + BlockKind::Diagram, + &["graph LR; tokens --> components --> app"], + ), + block( + BlockKind::Table { + columns: 3, + merged: Vec::new(), + }, + &[ + "Token", "Light", "Dark", "accent", "0.50", "0.62", "focus", "0.40", "0.55", + ], + ), + block( + BlockKind::Image(Media { + width: 0.7, + ..Media::new(ATRIUM) + }), + &["The atrium at noon"], + ), + block( + BlockKind::Columns(2), + &[ + "**Light**: paper white, ink near black.", + "**Dark**: warm charcoal, text off-white.", + ], + ), + block( + BlockKind::Synced("palette-note".into()), + &["Synced: edit either copy and both change."], + ), + block(BlockKind::Divider, &[]), + block( + BlockKind::Synced("palette-note".into()), + &["Synced: edit either copy and both change."], + ), + block( + BlockKind::Embed(Media::new("https://ely.dev")), + &["The project site"], + ), + ] +} + +pub fn editing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { + let rich = markdown("doc-notes", NOTES, 6, window, cx); + let essay = markdown("doc-essay", ESSAY, 24, window, cx); + let mode = keep("doc-mode", || MarkdownMode::Visual, window, cx); + let now_mode = *mode.read(cx); + let notes = window.use_keyed_state("doc-blocks", cx, |window, cx| { + BlockEditor::new(notes(), window, cx).people(PEOPLE) + }); + let kinds = window.use_keyed_state("doc-kinds", cx, |window, cx| { + BlockEditor::new(kinds(), window, cx) + .people(PEOPLE) + .diagrams(draw) + }); + let tasks = keep( + "doc-tasks", + || { + vec![ + ("Tokens in one place", true), + ("Light and dark", true), + ("Motion that settles", false), + ("A site that shows it all", false), + ] + }, + window, + cx, + ); + let now_tasks = tasks.read(cx).clone(); + div() + .child( + section( + "RichTextEditor / FixedFormatToolbar / FloatingFormatToolbar / LinkEditor / MentionMenu / EmojiAutocomplete / WordCount / ReadingTime", + "Markdown that reads as it is written: styles show as they are typed and their marks recede. The toolbar above and the one over a selection apply formats; ⌘K edits the link at the caret. @ suggests people, a colon and two letters suggest emoji. The foot counts words, characters and minutes.", + cx, + ) + .child(probe("doc-rich", div().w(px(760.)).child(RichTextEditor::new("doc-rich", &rich).people(PEOPLE)))), + ) + .child( + section( + "MarkdownEditor", + "One document three ways: visual, where it reads as it will and a pressed block opens as its source; source, styled as typed; and split, the source beside what it makes.", + cx, + ) + .child(probe( + "doc-markdown", + div().w(px(840.)).child( + MarkdownEditor::new("doc-markdown", &essay, now_mode) + .people(PEOPLE) + .on_mode(move |next, _, cx| set(&mode, next, cx)), + ), + )), + ) + .child( + section( + "BlockEditor / BlockHandle / SlashMenu / PlaceholderText", + "Blocks like a notebook's. Hover one for its handle: drag it to move the block, press it to turn, copy or delete it. / on an empty line lists every kind, and an empty line says so. Undo covers text and blocks alike.", + cx, + ) + .child(probe("doc-blocks", div().w(px(760.)).child(notes))), + ) + .child( + section( + "Block types / TableEditor / ImageEditorBlock / MathEditor / DiagramBlock / CodeBlockEditor", + "Code colored as code, math set as it is typed, diagrams the app draws from their source, dividers, tables that grow and merge cells, pictures to resize and align, pages to open, columns, and synced copies that change together.", + cx, + ) + .child(div().w(px(760.)).child(kinds)), + ) + .child( + section( + "TodoItem / Checklist", + "To-dos under a title, with how many are done as a count and a bar; a done one goes quiet and struck through.", + cx, + ) + .child( + div().w(px(360.)).child( + Checklist::new("doc-checklist", "Launch", now_tasks.clone()).on_toggle(move |ix, done, _, cx| { + let mut next = tasks.read(cx).clone(); + next[ix].1 = done; + set(&tasks, next, cx); + }), + ), + ), + ) +} diff --git a/examples/gallery/pages/documents/mod.rs b/examples/gallery/pages/documents/mod.rs new file mode 100644 index 0000000..f024add --- /dev/null +++ b/examples/gallery/pages/documents/mod.rs @@ -0,0 +1,65 @@ +mod editing; + +use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; + +use super::Page; +use crate::script::Step; + +pub const PAGE: Page = Page { + number: 21, + slug: "documents", + title: "Documents", + summary: "Markdown written and read, blocks like a notebook's, and a knowledge base around them.", + render, + script: SCRIPT, +}; + +const SCRIPT: &[Step] = &[ + Step::Rest, + Step::DownAt("doc-rich", 400.0, 203.0), + Step::UpAt("doc-rich", 400.0, 203.0), + Step::Wait(200), + Step::Key("cmd-shift-left"), + Step::Type("- [x] Light and dark @gr"), + Step::Wait(300), + Step::Shot("mention"), + Step::Key("escape"), + Step::Key("alt-shift-left"), + Step::Wait(300), + Step::Shot("floating"), + Step::Key("cmd-k"), + Step::Wait(300), + Step::Shot("link"), + Step::Key("escape"), + Step::Wait(200), + Step::DownAt("doc-markdown", 804.0, 16.0), + Step::UpAt("doc-markdown", 804.0, 16.0), + Step::Wait(300), + Step::Shot("split"), + Step::DownAt("doc-markdown", 696.0, 16.0), + Step::UpAt("doc-markdown", 696.0, 16.0), + Step::Wait(300), + Step::DownAt("doc-markdown", 38.0, 66.0), + Step::UpAt("doc-markdown", 38.0, 66.0), + Step::Wait(300), + Step::Shot("open-block"), + Step::Key("escape"), + Step::DownAt("doc-blocks", 740.0, 60.0), + Step::UpAt("doc-blocks", 740.0, 60.0), + Step::Wait(200), + Step::Key("enter"), + Step::Type("/"), + Step::Wait(300), + Step::Shot("slash"), + Step::Key("escape"), + Step::Key("cmd-z"), + Step::Key("cmd-z"), + Step::Wait(200), + Step::HoverAt("doc-blocks", 120.0, 20.0), + Step::Wait(300), + Step::Shot("handle"), +]; + +fn render(window: &mut Window, cx: &mut App) -> AnyElement { + div().child(editing::editing(window, cx)).into_any_element() +} diff --git a/examples/gallery/pages/mod.rs b/examples/gallery/pages/mod.rs index 81e3ef0..f344f25 100644 --- a/examples/gallery/pages/mod.rs +++ b/examples/gallery/pages/mod.rs @@ -2,6 +2,7 @@ mod buttons; mod charts; mod data; mod debug; +mod documents; mod editor; mod feedback; mod finance; @@ -55,6 +56,7 @@ pub const ALL: &[Page] = &[ terminal::PAGE, git::PAGE, debug::PAGE, + documents::PAGE, theme::PAGE, ]; diff --git a/src/documents/blocks/content.rs b/src/documents/blocks/content.rs new file mode 100644 index 0000000..77506e9 --- /dev/null +++ b/src/documents/blocks/content.rs @@ -0,0 +1,235 @@ +use gpui::{ + AnyElement, Context, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement, + SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, +}; + +use super::{Block, BlockEditor, BlockKind, media, table, view::Look}; +use crate::{ + buttons::CopyButton, + forms::Checkbox, + primitives::{Disclosure, Icon, IconName}, + theme::IconSize, + typography::Latex, +}; + +impl BlockEditor { + /// What a block shows, by kind. + pub(crate) fn content( + &self, + block: &Block, + number: usize, + look: &Look, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let colors = look.colors.clone(); + let (key, id) = (block.key, Self::id(cx)); + let marker = |text: SharedString| { + div() + .flex_none() + .min_w_5() + .text_color(colors.fg_muted) + .child(text) + }; + let row = |marker: AnyElement, field: AnyElement| { + div() + .flex() + .gap_1p5() + .child(marker) + .child(div().flex_1().min_w_0().child(field)) + }; + match &block.kind { + BlockKind::Paragraph => self.field_view(block, 0, window, cx), + BlockKind::Heading(level) => div() + .pt_2() + .text_size(look.headings[(*level as usize).clamp(1, 3) - 1]) + .font_weight(FontWeight::SEMIBOLD) + .child(self.field_view(block, 0, window, cx)) + .into_any_element(), + BlockKind::Bullet => row( + marker("•".into()).into_any_element(), + self.field_view(block, 0, window, cx), + ) + .into_any_element(), + BlockKind::Numbered => row( + marker(format!("{number}.").into()).into_any_element(), + self.field_view(block, 0, window, cx), + ) + .into_any_element(), + BlockKind::Todo(done) => { + let (entity, done) = (cx.entity(), *done); + let check = Checkbox::new((id.clone(), format!("todo-{key}")), done).on_change( + move |on, _, cx| { + entity.update(cx, |editor, cx| { + editor.set_kind(key, BlockKind::Todo(on), cx) + }) + }, + ); + row( + check.into_any_element(), + self.field_view(block, 0, window, cx), + ) + .when(done, |row| row.text_color(colors.fg_subtle)) + .into_any_element() + } + BlockKind::Quote => div() + .pl_3() + .border_l_2() + .border_color(colors.border_strong) + .text_color(colors.fg_muted) + .child(self.field_view(block, 0, window, cx)) + .into_any_element(), + BlockKind::Callout => div() + .flex() + .gap_2() + .p_3() + .rounded(look.radius) + .bg(colors.sunken) + .child( + Icon::new(IconName::Lightbulb) + .size(IconSize::Sm) + .color(colors.warning), + ) + .child( + div() + .flex_1() + .min_w_0() + .child(self.field_view(block, 0, window, cx)), + ) + .into_any_element(), + BlockKind::Toggle(open) => { + let (entity, open) = (cx.entity(), *open); + let chevron = div() + .id((id.clone(), format!("toggle-{key}"))) + .flex_none() + .cursor_pointer() + .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) + .on_click(move |_, _, cx| { + entity.update(cx, |editor, cx| { + editor.set_kind(key, BlockKind::Toggle(!open), cx) + }) + }) + .child(Disclosure::new( + (id.clone(), format!("disclosure-{key}")), + open, + )); + div() + .flex() + .flex_col() + .gap_1() + .child(row( + chevron.into_any_element(), + self.field_view(block, 0, window, cx), + )) + .when(open, |toggle| { + toggle.child(div().pl_6().child(self.field_view(block, 1, window, cx))) + }) + .into_any_element() + } + BlockKind::Code => { + let text = block.fields[0].read(cx).text().to_string(); + div() + .flex() + .flex_col() + .gap_1() + .p_3() + .rounded(look.radius) + .bg(colors.sunken) + .font_family(look.mono.clone()) + .text_size(look.small) + .child( + div() + .flex() + .justify_between() + .items_center() + .text_size(look.tiny) + .text_color(colors.fg_subtle) + .child("Code") + .child(CopyButton::new((id.clone(), format!("copy-{key}")), text)), + ) + .child(self.field_view(block, 0, window, cx)) + .into_any_element() + } + BlockKind::Math => { + let tex = block.fields[0].read(cx).text().to_string(); + let shown = match Latex::parse(&tex) { + _ if tex.trim().is_empty() => div().into_any_element(), + Ok(math) => div() + .flex() + .justify_center() + .py_2() + .child(math) + .into_any_element(), + Err(error) => div() + .text_color(colors.danger) + .child(error.to_string()) + .into_any_element(), + }; + div() + .flex() + .flex_col() + .gap_1() + .p_3() + .rounded(look.radius) + .border_1() + .border_color(colors.border) + .child( + div() + .font_family(look.mono.clone()) + .text_size(look.small) + .text_color(colors.fg_muted) + .child(self.field_view(block, 0, window, cx)), + ) + .child(shown) + .into_any_element() + } + BlockKind::Diagram => media::diagram(self, block, look, window, cx), + BlockKind::Divider => div() + .py_2() + .child(div().h_0().border_t_1().border_color(colors.border)) + .into_any_element(), + BlockKind::Table { .. } => table::table(self, block, look, window, cx), + BlockKind::Image(_) | BlockKind::Video(_) | BlockKind::Embed(_) => { + media::media(self, block, look, window, cx) + } + BlockKind::Columns(_) => div() + .flex() + .gap_4() + .children((0..block.fields.len()).map(|ix| { + div() + .flex_1() + .min_w_0() + .when(ix > 0, |column| { + column.pl_4().border_l_1().border_color(colors.border) + }) + .child(self.field_view(block, ix, window, cx)) + })) + .into_any_element(), + BlockKind::Synced(name) => div() + .flex() + .flex_col() + .gap_1() + .px_3() + .py_2() + .rounded(look.radius) + .border_1() + .border_color(colors.border) + .child( + div() + .flex() + .items_center() + .gap_1() + .text_size(look.tiny) + .text_color(colors.fg_subtle) + .child( + Icon::new(IconName::RefreshCw) + .size(IconSize::Xs) + .color(colors.fg_subtle), + ) + .child(format!("Synced · {name}")), + ) + .child(self.field_view(block, 0, window, cx)) + .into_any_element(), + } + } +} diff --git a/src/documents/blocks/edit.rs b/src/documents/blocks/edit.rs new file mode 100644 index 0000000..79b48d9 --- /dev/null +++ b/src/documents/blocks/edit.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +use gpui::{Context, Window}; + +use super::{Block, BlockData, BlockEditor, BlockEvent, BlockKind, Snapshot}; + +impl BlockEditor { + /// Splits a prose block at the caret: the text after it moves to a new block of the kind that follows. + pub(crate) fn split(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + let ix = self.index(key); + let kind = self.blocks[ix].kind.clone(); + let field = self.blocks[ix].fields[0].clone(); + if field.read(cx).is_empty() + && kind != BlockKind::Paragraph + && kind.next() != BlockKind::Paragraph + { + self.turn_into(key, BlockKind::Paragraph, window, cx); + return; + } + let caret = field.read(cx).cursor(); + let tail = field.read(cx).text()[caret..].to_string(); + log::info!("block editor: block {key} splits at {caret}"); + self.before_change(cx); + field.update(cx, |field, cx| { + let end = field.text().len(); + field.select(caret..end, cx); + field.insert("", cx); + }); + let key = self.fresh_key(); + let next = self.build( + BlockData { + kind: kind.next(), + texts: vec![tail], + }, + key, + window, + cx, + ); + self.blocks.insert(ix + 1, next); + self.after_change(cx); + self.focus_field(key, 0, Some(0), window, cx); + } + + /// Backspace at a prose block's start: another kind turns back into text; text joins the prose above, or an empty block goes. + pub(crate) fn join(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + let ix = self.index(key); + if self.blocks[ix].kind != BlockKind::Paragraph { + self.turn_into(key, BlockKind::Paragraph, window, cx); + return; + } + let text = self.blocks[ix].fields[0].read(cx).text().to_string(); + let Some(above) = ix.checked_sub(1).map(|ix| &self.blocks[ix]) else { + return; + }; + if !above.kind.is_prose() { + if text.is_empty() { + self.remove(key, window, cx); + } + return; + } + let (above_key, above_field) = (above.key, above.fields[0].clone()); + log::info!("block editor: block {key} joins block {above_key}"); + self.before_change(cx); + let joint = above_field.read(cx).text().len(); + above_field.update(cx, |field, cx| { + field.select(joint..joint, cx); + field.insert(&text, cx); + }); + self.blocks.remove(ix); + self.after_change(cx); + self.focus_field(above_key, 0, Some(joint), window, cx); + } + + pub(crate) fn undo(&mut self, window: &mut Window, cx: &mut Context) { + self.settle(true, cx); + if let Some(previous) = self.history.undo(self.current.clone()) { + log::info!("block editor: undo"); + self.restore(previous, window, cx); + } + } + + pub(crate) fn redo(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(next) = self.history.redo(self.current.clone()) { + log::info!("block editor: redo"); + self.restore(next, window, cx); + } + } + + /// Puts the document back as `snapshot` holds it, keeping fields whose blocks stay. + fn restore(&mut self, snapshot: Snapshot, window: &mut Window, cx: &mut Context) { + let mut kept: HashMap = self + .blocks + .drain(..) + .map(|block| (block.key, block)) + .collect(); + for (key, data) in &snapshot.0 { + let block = match kept.remove(key) { + Some(mut block) if block.fields.len() == data.texts.len() => { + block.kind = data.kind.clone(); + for (field, text) in block.fields.iter().zip(&data.texts) { + if field.read(cx).text() != text { + field.update(cx, |field, cx| field.set_text(text.clone(), cx)); + } + } + block + } + _ => self.build(data.clone(), *key, window, cx), + }; + self.blocks.push(block); + } + self.current = snapshot; + cx.emit(BlockEvent::Changed); + cx.notify(); + } +} diff --git a/src/documents/blocks/keys.rs b/src/documents/blocks/keys.rs new file mode 100644 index 0000000..5b567f5 --- /dev/null +++ b/src/documents/blocks/keys.rs @@ -0,0 +1,144 @@ +use gpui::{Context, Window}; + +use super::{BlockData, BlockEditor, BlockKind}; +use crate::documents::modes::at_edge; + +/// A new empty paragraph. +fn paragraph() -> BlockData { + BlockData::new(BlockKind::Paragraph, [""]) +} + +impl BlockEditor { + /// The fields in reading order, as `(block, field)`: a closed toggle hides its body. + fn reading_order(&self) -> Vec<(u64, usize)> { + self.blocks + .iter() + .flat_map(|block| { + let shown = match block.kind { + BlockKind::Toggle(false) => 1, + _ => block.fields.len(), + }; + (0..shown).map(move |field| (block.key, field)) + }) + .collect() + } + + /// Enter in a field: prose splits, a toggle's title opens its body, a caption starts text below, a cell steps down a row; elsewhere Enter breaks the line. + pub(crate) fn on_enter( + &mut self, + key: u64, + field: usize, + window: &mut Window, + cx: &mut Context, + ) -> bool { + let kind = self.blocks[self.index(key)].kind.clone(); + match kind { + kind if kind.is_prose() => self.split(key, window, cx), + BlockKind::Toggle(open) if field == 0 => { + if !open { + self.set_kind(key, BlockKind::Toggle(true), cx); + } + self.focus_field(key, 1, None, window, cx); + } + BlockKind::Image(_) | BlockKind::Video(_) | BlockKind::Embed(_) => { + self.insert(Some(key), paragraph(), window, cx); + } + BlockKind::Table { columns, .. } => { + let below = field + columns; + if below >= self.blocks[self.index(key)].fields.len() { + self.add_row(key, window, cx); + } + self.focus_field(key, below, None, window, cx); + } + _ => return false, + } + true + } + + /// Backspace at a field's start: prose joins up or turns back into text; an empty code, math or diagram block turns into text. + pub(crate) fn on_backspace( + &mut self, + key: u64, + field: usize, + window: &mut Window, + cx: &mut Context, + ) -> bool { + let block = &self.blocks[self.index(key)]; + let input = block.fields[field].read(cx); + if input.cursor() != 0 || !input.selection().is_empty() { + return false; + } + let empty = input.is_empty(); + match &block.kind { + kind if kind.is_prose() => self.join(key, window, cx), + kind if kind.is_source() && empty => { + self.turn_into(key, BlockKind::Paragraph, window, cx) + } + BlockKind::Toggle(_) if field == 0 && block.fields[1].read(cx).is_empty() => { + self.turn_into(key, BlockKind::Paragraph, window, cx) + } + _ => return false, + } + true + } + + /// Up or Down on a field's first or last line moves to the field above or below, a table cell by a row. + pub(crate) fn on_vertical( + &mut self, + key: u64, + field: usize, + down: bool, + window: &mut Window, + cx: &mut Context, + ) -> bool { + let block = &self.blocks[self.index(key)]; + if !at_edge(block.fields[field].read(cx), down) { + return false; + } + if let BlockKind::Table { columns, .. } = block.kind { + let row = if down { + Some(field + columns) + } else { + field.checked_sub(columns) + }; + if let Some(row) = row.filter(|row| *row < block.fields.len()) { + self.focus_field(key, row, None, window, cx); + return true; + } + } + let order = self.reading_order(); + let at = order + .iter() + .position(|place| *place == (key, field)) + .expect("a focused field is in reading order"); + let target = if down { + order[at + 1..].iter().find(|(other, _)| *other != key) + } else { + order[..at].iter().rev().find(|(other, _)| *other != key) + }; + let Some(&(target, target_field)) = target else { + if down && !self.blocks.last().is_some_and(|last| last.kind.is_prose()) { + self.insert(Some(key), paragraph(), window, cx); + return true; + } + return false; + }; + let end = self.blocks[self.index(target)].fields[target_field] + .read(cx) + .text() + .len(); + self.focus_field( + target, + target_field, + Some(if down { 0 } else { end }), + window, + cx, + ); + true + } + + /// The submit key in any field leaves the block for a new paragraph below. + pub(crate) fn on_submit(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + self.insert(Some(key), paragraph(), window, cx); + } +} diff --git a/src/documents/blocks/kind.rs b/src/documents/blocks/kind.rs new file mode 100644 index 0000000..de33b09 --- /dev/null +++ b/src/documents/blocks/kind.rs @@ -0,0 +1,277 @@ +use gpui::SharedString; + +use crate::primitives::IconName; + +/// Where a picture sits across its column. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Align { + Left, + #[default] + Center, + Right, +} + +/// A picture, clip or page shown in a block: its address, and its width as a share of the column. +#[derive(Clone, Debug, PartialEq)] +pub struct Media { + pub source: SharedString, + pub width: f32, + pub align: Align, +} + +impl Media { + pub fn new(source: impl Into) -> Self { + Self { + source: source.into(), + width: 1.0, + align: Align::Center, + } + } +} + +/// What a block is. Each holds its text in fields: one for most, a title and a body for a toggle, one per column, a caption for media, one per cell for a table, none for a divider. +#[derive(Clone, Debug, PartialEq)] +pub enum BlockKind { + Paragraph, + Heading(u8), + Bullet, + Numbered, + Todo(bool), + Quote, + Callout, + /// Open or closed; its body shows while open. + Toggle(bool), + Code, + Math, + /// Diagram source, such as Mermaid; the host renders the picture. + Diagram, + Divider, + /// Columns across, the header row first; each `merged` cell, as row and column, folds into the cell on its left. + Table { + columns: usize, + merged: Vec<(usize, usize)>, + }, + Image(Media), + Video(Media), + Embed(Media), + /// Side by side, one field each. + Columns(usize), + /// Text shared with every other block synced to the same source. + Synced(SharedString), +} + +impl BlockKind { + /// Its name in the slash menu. + pub fn label(&self) -> &'static str { + match self { + Self::Paragraph => "Text", + Self::Heading(1) => "Heading 1", + Self::Heading(2) => "Heading 2", + Self::Heading(_) => "Heading 3", + Self::Bullet => "Bulleted list", + Self::Numbered => "Numbered list", + Self::Todo(_) => "To-do", + Self::Quote => "Quote", + Self::Callout => "Callout", + Self::Toggle(_) => "Toggle", + Self::Code => "Code", + Self::Math => "Math", + Self::Diagram => "Diagram", + Self::Divider => "Divider", + Self::Table { .. } => "Table", + Self::Image(_) => "Image", + Self::Video(_) => "Video", + Self::Embed(_) => "Embed", + Self::Columns(_) => "Columns", + Self::Synced(_) => "Synced block", + } + } + + pub fn icon(&self) -> IconName { + match self { + Self::Paragraph => IconName::Type, + Self::Heading(1) => IconName::Heading1, + Self::Heading(2) => IconName::Heading2, + Self::Heading(_) => IconName::Heading3, + Self::Bullet => IconName::List, + Self::Numbered => IconName::ListOrdered, + Self::Todo(_) => IconName::ListTodo, + Self::Quote => IconName::Quote, + Self::Callout => IconName::Info, + Self::Toggle(_) => IconName::ChevronRight, + Self::Code => IconName::Code, + Self::Math => IconName::SquareFunction, + Self::Diagram => IconName::Workflow, + Self::Divider => IconName::Minus, + Self::Table { .. } => IconName::Table, + Self::Image(_) => IconName::Image, + Self::Video(_) => IconName::Video, + Self::Embed(_) => IconName::Globe, + Self::Columns(_) => IconName::Columns2, + Self::Synced(_) => IconName::RefreshCw, + } + } + + /// What an empty field of this kind says while it has focus. + pub fn placeholder(&self) -> &'static str { + match self { + Self::Paragraph => "Type '/' for commands", + Self::Heading(_) => self.label(), + Self::Bullet | Self::Numbered => "List", + Self::Todo(_) => "To-do", + Self::Quote => "Quote", + Self::Callout | Self::Toggle(_) | Self::Columns(_) | Self::Synced(_) => "Write", + Self::Code => "Code", + Self::Math => "A formula in TeX, such as e^{i\\pi} + 1 = 0", + Self::Diagram => "A diagram in Mermaid, such as graph LR; a --> b", + Self::Image(_) | Self::Video(_) | Self::Embed(_) => "Add a caption", + Self::Divider | Self::Table { .. } => "", + } + } + + /// How many fields a new block of this kind holds. + pub fn fields(&self) -> usize { + match self { + Self::Divider => 0, + Self::Toggle(_) => 2, + Self::Columns(count) => *count, + Self::Table { columns, .. } => columns * 2, + _ => 1, + } + } + + /// Whether it can hold `count` fields: a table any whole rows past its header, the rest their own number. + pub fn accepts(&self, count: usize) -> bool { + match self { + Self::Table { columns, .. } => { + *columns > 0 && count >= columns * 2 && count.is_multiple_of(*columns) + } + kind => count == kind.fields(), + } + } + + /// Prose: Enter splits it, Backspace at its start turns it back into text or joins it up. + pub fn is_prose(&self) -> bool { + matches!( + self, + Self::Paragraph + | Self::Heading(_) + | Self::Bullet + | Self::Numbered + | Self::Todo(_) + | Self::Quote + | Self::Callout + ) + } + + /// The kind Enter starts after this one: lists go on, the rest return to text. + pub fn next(&self) -> Self { + match self { + Self::Bullet => Self::Bullet, + Self::Numbered => Self::Numbered, + Self::Todo(_) => Self::Todo(false), + _ => Self::Paragraph, + } + } + + /// Lines of monospaced source, where Enter breaks a line. + pub fn is_source(&self) -> bool { + matches!(self, Self::Code | Self::Math | Self::Diagram) + } +} + +/// The kinds the slash menu offers, in order; media and synced blocks come from their owner. +pub(crate) fn offered() -> Vec { + vec![ + BlockKind::Paragraph, + BlockKind::Heading(1), + BlockKind::Heading(2), + BlockKind::Heading(3), + BlockKind::Bullet, + BlockKind::Numbered, + BlockKind::Todo(false), + BlockKind::Quote, + BlockKind::Callout, + BlockKind::Toggle(true), + BlockKind::Code, + BlockKind::Math, + BlockKind::Diagram, + BlockKind::Divider, + BlockKind::Table { + columns: 2, + merged: Vec::new(), + }, + BlockKind::Columns(2), + ] +} + +/// The kinds whose name holds `query`, for the slash menu. +pub(crate) fn matching(query: &str) -> Vec { + let query = query.to_lowercase(); + offered() + .into_iter() + .filter(|kind| kind.label().to_lowercase().contains(&query)) + .collect() +} + +/// A paragraph's leading markdown that makes it another kind, and how many bytes it takes; the space after it is typed last. +pub(crate) fn shortcut(text: &str) -> Option<(BlockKind, usize)> { + let prefixes: [(&str, BlockKind); 11] = [ + ("# ", BlockKind::Heading(1)), + ("## ", BlockKind::Heading(2)), + ("### ", BlockKind::Heading(3)), + ("- ", BlockKind::Bullet), + ("* ", BlockKind::Bullet), + ("1. ", BlockKind::Numbered), + ("[] ", BlockKind::Todo(false)), + ("[ ] ", BlockKind::Todo(false)), + ("[x] ", BlockKind::Todo(true)), + ("> ", BlockKind::Quote), + ("``` ", BlockKind::Code), + ]; + if text == "---" { + return Some((BlockKind::Divider, 3)); + } + prefixes + .into_iter() + .find(|(prefix, _)| text.starts_with(prefix)) + .map(|(prefix, kind)| (kind, prefix.len())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn markdown_at_a_paragraphs_start_turns_it_into_a_kind() { + assert_eq!(shortcut("## Lift"), Some((BlockKind::Heading(2), 3))); + assert_eq!(shortcut("[x] done"), Some((BlockKind::Todo(true), 4))); + assert_eq!(shortcut("---"), Some((BlockKind::Divider, 3))); + assert_eq!(shortcut("#hashtag"), None); + assert_eq!(shortcut("1.5 pages"), None); + } + + #[test] + fn the_slash_menu_filters_by_name() { + let labels: Vec<&str> = matching("head").iter().map(BlockKind::label).collect(); + assert_eq!(labels, ["Heading 1", "Heading 2", "Heading 3"]); + assert!(matching("zzz").is_empty()); + } + + #[test] + fn a_table_holds_whole_rows_past_its_header() { + let table = BlockKind::Table { + columns: 3, + merged: Vec::new(), + }; + assert!(table.accepts(9)); + assert!(!table.accepts(7) && !table.accepts(3)); + assert!(BlockKind::Toggle(true).accepts(2) && !BlockKind::Paragraph.accepts(2)); + } + + #[test] + fn lists_go_on_and_the_rest_return_to_text() { + assert_eq!(BlockKind::Todo(true).next(), BlockKind::Todo(false)); + assert_eq!(BlockKind::Heading(1).next(), BlockKind::Paragraph); + } +} diff --git a/src/documents/blocks/media.rs b/src/documents/blocks/media.rs new file mode 100644 index 0000000..af07391 --- /dev/null +++ b/src/documents/blocks/media.rs @@ -0,0 +1,371 @@ +use std::{path::Path, sync::Arc}; + +use gpui::{ + AnyElement, AppContext as _, Bounds, Context, EmptyView, EntityId, Image, ImageFormat, + ImageSource, InteractiveElement, IntoElement, ParentElement, Pixels, SharedUri, + StatefulInteractiveElement, Styled, Window, div, img, prelude::*, +}; +use regex::Regex; + +use super::{Align, Block, BlockEditor, BlockKind, Media, view::Look}; +use crate::{ + buttons::{Button, ButtonVariant, IconButton}, + primitives::{Icon, IconName, Measure}, + theme::{ControlSize, IconSize}, +}; + +/// Narrowest a picture gets, as a share of its column. +const NARROWEST: f32 = 0.2; + +/// A picture's edge on its way, and whose. +struct Resize { + owner: EntityId, + key: u64, +} + +/// A web address loads over the network; anything else is a file. +pub(crate) fn source(text: &str) -> ImageSource { + if text.starts_with("http://") || text.starts_with("https://") { + SharedUri::from(text.to_string()).into() + } else { + Path::new(text).into() + } +} + +/// The share of `column` a picture set by `align` spans when its edge is at `x`. +pub(crate) fn share(align: Align, column: Bounds, x: Pixels) -> f32 { + let width = f32::from(column.size.width).max(1.0); + let reach = match align { + Align::Left => f32::from(x - column.left()), + Align::Right => f32::from(column.right() - x), + Align::Center => 2.0 * f32::from(x - column.center().x).abs(), + }; + (reach / width).clamp(NARROWEST, 1.0) +} + +fn media_of(kind: &mut BlockKind) -> &mut Media { + match kind { + BlockKind::Image(media) | BlockKind::Video(media) | BlockKind::Embed(media) => media, + other => unreachable!("{} holds no media", other.label()), + } +} + +impl BlockEditor { + /// Sets a picture's width while its edge is dragged; the drag's start records undo. + fn resize(&mut self, key: u64, width: f32, first: bool, cx: &mut Context) { + if first { + self.before_change(cx); + } + let ix = self.index(key); + media_of(&mut self.blocks[ix].kind).width = width; + self.after_change(cx); + } + + pub(crate) fn align(&mut self, key: u64, align: Align, cx: &mut Context) { + let ix = self.index(key); + let mut kind = self.blocks[ix].kind.clone(); + media_of(&mut kind).align = align; + self.set_kind(key, kind, cx); + } +} + +/// A picture sized as a share of its column, set left, center or right, with an edge to drag and a caption below; a clip or a page shows as a card to open. +pub(super) fn media( + editor: &BlockEditor, + block: &Block, + look: &Look, + window: &mut Window, + cx: &mut Context, +) -> AnyElement { + let (key, id) = (block.key, BlockEditor::id(cx)); + let colors = look.colors.clone(); + let caption = div() + .text_size(look.small) + .text_color(colors.fg_muted) + .child(editor.field_view(block, 0, window, cx)); + let (media, icon) = match &block.kind { + BlockKind::Image(media) => (media.clone(), None), + BlockKind::Video(media) => (media.clone(), Some(IconName::Video)), + BlockKind::Embed(media) => (media.clone(), Some(IconName::Globe)), + other => unreachable!("{} is no media", other.label()), + }; + if let Some(icon) = icon { + let address = media.source.clone(); + return div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .flex() + .items_center() + .gap_3() + .p_3() + .rounded(look.radius) + .border_1() + .border_color(colors.border) + .child(Icon::new(icon).size(IconSize::Md).color(colors.fg_muted)) + .child( + div() + .flex_1() + .min_w_0() + .text_color(colors.fg_muted) + .child(media.source.clone()), + ) + .child( + Button::new((id.clone(), format!("open-{key}")), "Open") + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .on_click(move |_, _, cx| cx.open_url(&address)), + ), + ) + .child(caption) + .into_any_element(); + } + let column = window.use_keyed_state((id.clone(), format!("column-{key}")), cx, |_, _| { + Bounds::::default() + }); + let bounds = *column.read(cx); + let (align, width) = (media.align, media.width); + let owner = cx.entity_id(); + let edge = |side: &'static str, cx: &mut Context| { + let entity = cx.entity(); + div() + .id((id.clone(), format!("edge-{key}-{side}"))) + .absolute() + .top_0() + .bottom_0() + .map(|edge| { + if side == "left" { + edge.left_0() + } else { + edge.right_0() + } + }) + .w_2() + .cursor_col_resize() + .on_drag(Resize { owner, key }, move |_, _, _, cx| { + entity.update(cx, |editor, cx| editor.resize(key, width, true, cx)); + log::info!("image {key}: resize"); + cx.new(|_| EmptyView) + }) + }; + let aligned = |align_to: Align, icon: IconName, cx: &mut Context| { + IconButton::new((id.clone(), format!("align-{key}-{align_to:?}")), icon) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .tooltip(format!("Align {align_to:?}").to_lowercase()) + .on_click(cx.listener(move |editor, _, _, cx| editor.align(key, align_to, cx))) + }; + let tools = div() + .absolute() + .top_1() + .right_1() + .flex() + .gap_0p5() + .p_0p5() + .rounded(look.radius) + .bg(colors.overlay) + .opacity(0.0) + .group_hover(format!("picture-{key}"), |tools| tools.opacity(1.0)) + .child(aligned(Align::Left, IconName::AlignLeft, cx)) + .child(aligned(Align::Center, IconName::AlignCenter, cx)) + .child(aligned(Align::Right, IconName::AlignRight, cx)); + let picture = div() + .id((id.clone(), format!("picture-{key}"))) + .group(format!("picture-{key}")) + .relative() + .w(bounds.size.width * width) + .child( + img(source(&media.source)) + .id((id.clone(), format!("img-{key}"))) + .w(bounds.size.width * width) + .rounded(look.radius), + ) + .child(edge("left", cx)) + .child(edge("right", cx)) + .child(tools); + let entity = cx.entity(); + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .id((id.clone(), format!("column-{key}"))) + .w_full() + .on_drag_move(move |event: &gpui::DragMoveEvent, _, cx| { + if event.drag(cx).owner != owner || event.drag(cx).key != key { + return; + } + let width = share(align, event.bounds, event.event.position.x); + entity.update(cx, |editor, cx| editor.resize(key, width, false, cx)); + }) + .child( + Measure::new( + (id.clone(), format!("measure-{key}")), + move |measured, _, cx| { + column.update(cx, |column, cx| { + *column = measured; + cx.notify(); + }) + }, + ) + .w_full() + .flex() + .map(|row| match align { + Align::Left => row.justify_start(), + Align::Center => row.justify_center(), + Align::Right => row.justify_end(), + }) + .child( + div() + .flex() + .flex_col() + .gap_1() + .w(bounds.size.width * width) + .child(picture) + .child(caption), + ), + ), + ) + .into_any_element() +} + +/// A diagram the host drew, and the width its SVG asks for. +#[derive(Clone)] +struct Drawn { + image: Arc, + width: Option, +} + +/// The width an SVG's root asks for, in pixels, when it gives one. +pub(crate) fn svg_width(svg: &str) -> Option { + let root = Regex::new(r#"]*?\swidth=['"]([0-9.]+)(px)?['"]"#) + .expect("the SVG width pattern compiles"); + root.captures(svg)?.get(1)?.as_str().parse().ok() +} + +/// A diagram's source above the picture the host draws from it. +pub(super) fn diagram( + editor: &BlockEditor, + block: &Block, + look: &Look, + window: &mut Window, + cx: &mut Context, +) -> AnyElement { + let (key, id) = (block.key, BlockEditor::id(cx)); + let text = block.fields[0].read(cx).text().to_string(); + let drawn = window.use_keyed_state((id.clone(), format!("drawn-{key}")), cx, |_, _| { + None::<(String, Result)> + }); + let stale = drawn + .read(cx) + .as_ref() + .is_none_or(|(source, _)| *source != text); + if stale && let Some(render) = &editor.diagram { + let picture = render(&text).map(|svg| Drawn { + width: svg_width(&svg), + image: Arc::new(Image::from_bytes(ImageFormat::Svg, svg.into_bytes())), + }); + log::info!( + "diagram {key}: drawn, {}", + if picture.is_ok() { + "fine" + } else { + "with an error" + } + ); + drawn.update(cx, |drawn, _| *drawn = Some((text.clone(), picture))); + } + let colors = look.colors.clone(); + let shown: AnyElement = match (editor.diagram.is_some(), drawn.read(cx).clone()) { + _ if text.trim().is_empty() => div().into_any_element(), + (false, _) => div() + .text_color(colors.fg_subtle) + .child("Diagrams draw once the app gives a renderer") + .into_any_element(), + (true, Some((_, Ok(drawn)))) => { + let column = + window.use_keyed_state((id.clone(), format!("diagram-width-{key}")), cx, |_, _| { + Pixels::ZERO + }); + let width = drawn.width.map_or(*column.read(cx), |natural| { + Pixels::from(natural).min(*column.read(cx)) + }); + Measure::new( + (id.clone(), format!("diagram-measure-{key}")), + move |bounds, _, cx| { + column.update(cx, |column, cx| { + *column = bounds.size.width; + cx.notify(); + }) + }, + ) + .w_full() + .flex() + .justify_center() + .child( + img(drawn.image) + .id((id.clone(), format!("diagram-{key}"))) + .w(width), + ) + .into_any_element() + } + (true, Some((_, Err(error)))) => div() + .text_color(colors.danger) + .child(error) + .into_any_element(), + (true, None) => unreachable!("a renderer draws before this shows"), + }; + div() + .flex() + .flex_col() + .gap_2() + .p_3() + .rounded(look.radius) + .border_1() + .border_color(colors.border) + .child( + div() + .font_family(look.mono.clone()) + .text_size(look.small) + .text_color(colors.fg_muted) + .child(editor.field_view(block, 0, window, cx)), + ) + .child(shown) + .into_any_element() +} + +#[cfg(test)] +mod tests { + use gpui::{Bounds, point, px, size}; + + use super::*; + + #[test] + fn an_svg_gives_its_width_when_it_has_one() { + assert_eq!( + svg_width(""), + Some(504.0) + ); + assert_eq!(svg_width(""), Some(120.0)); + assert_eq!( + svg_width(""), + None + ); + } + + #[test] + fn an_edge_sets_its_share_by_how_the_picture_sits() { + let column = Bounds::new(point(px(100.0), px(0.0)), size(px(400.0), px(10.0))); + assert_eq!(share(Align::Left, column, px(300.0)), 0.5); + assert_eq!(share(Align::Right, column, px(300.0)), 0.5); + assert_eq!(share(Align::Center, column, px(400.0)), 0.5); + assert_eq!( + share(Align::Center, column, px(300.0)), + NARROWEST, + "a picture keeps some width" + ); + } +} diff --git a/src/documents/blocks/mod.rs b/src/documents/blocks/mod.rs new file mode 100644 index 0000000..b0798e5 --- /dev/null +++ b/src/documents/blocks/mod.rs @@ -0,0 +1,479 @@ +mod content; +mod edit; +mod keys; +mod kind; +mod media; +mod table; +mod view; + +use std::{collections::HashMap, rc::Rc}; + +use gpui::{ + App, AppContext as _, Context, Entity, EventEmitter, Focusable, Global, SharedString, + Subscription, Window, +}; + +pub use kind::{Align, BlockKind, Media}; +pub(crate) use kind::{matching, shortcut}; + +use super::markdown::markdown_highlights; +use crate::forms::{History, InputEvent, TextInput, code_highlights}; + +/// A block as the editor gives it out and takes it in: its kind and the text of each of its fields. +#[derive(Clone, Debug, PartialEq)] +pub struct BlockData { + pub kind: BlockKind, + pub texts: Vec, +} + +impl BlockData { + pub fn new(kind: BlockKind, texts: impl IntoIterator>) -> Self { + let texts: Vec = texts.into_iter().map(Into::into).collect(); + assert!( + kind.accepts(texts.len()), + "a {} block cannot hold {} fields", + kind.label(), + texts.len() + ); + Self { kind, texts } + } +} + +/// What a block editor tells its owner. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockEvent { + Changed, +} + +/// Renders a diagram's source to SVG, or says what is wrong with it. +pub(crate) type RenderDiagram = Rc Result>; + +/// Text shared by synced blocks, by name, across every editor in the app. +#[derive(Default)] +struct SyncedTexts(HashMap); + +impl Global for SyncedTexts {} + +pub(crate) struct Block { + pub key: u64, + pub kind: BlockKind, + pub fields: Vec>, + _changes: Vec, +} + +/// The whole document, as undo keeps it. +#[derive(Clone, Debug, PartialEq)] +struct Snapshot(Vec<(u64, BlockData)>); + +/// A document of blocks, Notion-like: prose, lists and to-dos, quotes and callouts, toggles, code, math, diagrams, dividers, tables, media, columns and synced blocks. Markdown at a paragraph's start changes its kind; `/` offers every kind; `@` and `:` suggest people and emoji; blocks drag by their handle. Undo covers text and structure alike. +pub struct BlockEditor { + pub(crate) blocks: Vec, + next: u64, + history: History, + current: Snapshot, + pub(crate) people: Vec, + pub(crate) diagram: Option, + _synced: Subscription, +} + +impl EventEmitter for BlockEditor {} + +impl BlockEditor { + pub fn new(blocks: Vec, window: &mut Window, cx: &mut Context) -> Self { + let synced = cx.observe_global_in::(window, |editor, _, cx| editor.pull(cx)); + let mut editor = Self { + blocks: Vec::new(), + next: 0, + history: History::default(), + current: Snapshot(Vec::new()), + people: Vec::new(), + diagram: None, + _synced: synced, + }; + for data in blocks { + let key = editor.fresh_key(); + let block = editor.build(data, key, window, cx); + editor.blocks.push(block); + } + editor.current = editor.snapshot(cx); + editor + } + + /// Handles `@` suggests. + pub fn people(mut self, handles: impl IntoIterator>) -> Self { + self.people = handles.into_iter().map(Into::into).collect(); + self + } + + /// Draws diagram blocks: their source to SVG, or an error to show in its place. + pub fn diagrams(mut self, render: impl Fn(&str) -> Result + 'static) -> Self { + self.diagram = Some(Rc::new(render)); + self + } + + /// The document as it stands. + pub fn blocks(&self, cx: &App) -> Vec { + self.snapshot(cx) + .0 + .into_iter() + .map(|(_, data)| data) + .collect() + } + + fn snapshot(&self, cx: &App) -> Snapshot { + Snapshot( + self.blocks + .iter() + .map(|block| { + let texts = block + .fields + .iter() + .map(|field| field.read(cx).text().to_string()) + .collect(); + ( + block.key, + BlockData { + kind: block.kind.clone(), + texts, + }, + ) + }) + .collect(), + ) + } + + /// A field for `kind`: inline markdown styled as typed, or code colored as code. + fn field( + kind: &BlockKind, + text: &str, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let code = *kind == BlockKind::Code; + let field = cx.new(|cx| { + let hint = match kind { + BlockKind::Paragraph | BlockKind::Table { .. } => "", + kind => kind.placeholder(), + }; + let field = TextInput::new(window, cx) + .multi_line(1, usize::MAX) + .placeholder(hint); + match (code, kind.is_source()) { + (true, _) => field.highlighter(code_highlights), + (false, true) => field, + (false, false) => field.highlighter(markdown_highlights), + } + }); + field.update(cx, |field, cx| field.set_text(text, cx)); + field + } + + fn fresh_key(&mut self) -> u64 { + self.next += 1; + self.next + } + + fn build( + &mut self, + data: BlockData, + key: u64, + window: &mut Window, + cx: &mut Context, + ) -> Block { + assert!( + data.kind.accepts(data.texts.len()), + "a {} block cannot hold {} fields", + data.kind.label(), + data.texts.len() + ); + let mut texts = data.texts; + if let BlockKind::Synced(name) = &data.kind { + let shared = cx + .try_global::() + .and_then(|synced| synced.0.get(name).cloned()); + match shared { + Some(shared) => texts[0] = shared, + None => { + cx.default_global::() + .0 + .insert(name.clone(), texts[0].clone()); + } + } + } + let fields: Vec> = texts + .iter() + .map(|text| Self::field(&data.kind, text, window, cx)) + .collect(); + let changes = fields + .iter() + .map(|field| Self::watch(key, field, window, cx)) + .collect(); + Block { + key, + kind: data.kind, + fields, + _changes: changes, + } + } + + fn watch( + key: u64, + field: &Entity, + window: &mut Window, + cx: &mut Context, + ) -> Subscription { + cx.subscribe_in(field, window, move |editor, field, event, window, cx| { + editor.on_field(key, field, *event, window, cx) + }) + } + + /// Adds an empty field to a block, at `at` or last. + pub(crate) fn add_field( + &mut self, + key: u64, + at: Option, + window: &mut Window, + cx: &mut Context, + ) { + let ix = self.index(key); + let field = Self::field(&self.blocks[ix].kind, "", window, cx); + let watched = Self::watch(key, &field, window, cx); + let block = &mut self.blocks[ix]; + let at = at.unwrap_or(block.fields.len()); + block.fields.insert(at, field); + block._changes.insert(at, watched); + } + + pub(crate) fn remove_field(&mut self, key: u64, field: usize) { + let ix = self.index(key); + let block = &mut self.blocks[ix]; + block.fields.remove(field); + drop(block._changes.remove(field)); + } + + pub(crate) fn index(&self, key: u64) -> usize { + self.blocks + .iter() + .position(|block| block.key == key) + .unwrap_or_else(|| panic!("block {key} left the editor")) + } + + fn on_field( + &mut self, + key: u64, + field: &Entity, + event: InputEvent, + window: &mut Window, + cx: &mut Context, + ) { + let ix = self.index(key); + match event { + InputEvent::Changed => { + let typed = field.focus_handle(cx).is_focused(window); + if typed + && self.blocks[ix].kind == BlockKind::Paragraph + && let Some((kind, length)) = shortcut(field.read(cx).text()) + && field.read(cx).cursor() == length + { + log::info!( + "block editor: markdown turns block {key} into {}", + kind.label() + ); + field.update(cx, |field, cx| { + field.select(0..length, cx); + field.insert("", cx); + }); + self.turn_into(key, kind, window, cx); + return; + } + if let BlockKind::Synced(name) = &self.blocks[ix].kind { + let text = field.read(cx).text().to_string(); + let shared = cx + .try_global::() + .and_then(|synced| synced.0.get(name)); + if shared != Some(&text) { + log::info!("block editor: synced {name} written"); + cx.default_global::() + .0 + .insert(name.clone(), text); + } + } + self.settle(true, cx); + } + InputEvent::Focus if field.read(cx).is_empty() => { + let hint = self.blocks[ix].kind.placeholder(); + field.update(cx, |field, cx| field.set_placeholder(hint, cx)); + } + InputEvent::Blur if self.blocks[ix].kind == BlockKind::Paragraph => { + field.update(cx, |field, cx| field.set_placeholder("", cx)); + } + _ => {} + } + } + + /// Takes in synced text written elsewhere. + fn pull(&mut self, cx: &mut Context) { + let shared: Vec<(Entity, String)> = self + .blocks + .iter() + .filter_map(|block| match &block.kind { + BlockKind::Synced(name) => cx + .try_global::() + .and_then(|synced| synced.0.get(name)) + .map(|text| (block.fields[0].clone(), text.clone())), + _ => None, + }) + .collect(); + for (field, text) in shared { + if field.read(cx).text() != text { + log::debug!("block editor: synced text arrived"); + field.update(cx, |field, cx| field.set_text(text, cx)); + } + } + } + + /// Records what changed as one undo step, a burst of typing as one, and tells the owner. + fn settle(&mut self, typing: bool, cx: &mut Context) { + let now = self.snapshot(cx); + if now != self.current { + let before = std::mem::replace(&mut self.current, now); + self.history.record(before, typing); + cx.emit(BlockEvent::Changed); + cx.notify(); + } + } + + /// Records the document before a change to its blocks. + pub(crate) fn before_change(&mut self, cx: &mut Context) { + self.settle(true, cx); + self.history.record(self.current.clone(), false); + } + + /// After a change to the blocks: the new state is current. + pub(crate) fn after_change(&mut self, cx: &mut Context) { + self.current = self.snapshot(cx); + cx.emit(BlockEvent::Changed); + cx.notify(); + } + + pub(crate) fn focus_field( + &self, + key: u64, + field: usize, + at: Option, + window: &mut Window, + cx: &mut App, + ) { + let block = &self.blocks[self.index(key)]; + let Some(field) = block.fields.get(field) else { + return; + }; + if let Some(at) = at { + field.update(cx, |field, cx| field.select(at..at, cx)); + } + window.focus(&field.focus_handle(cx)); + } + + /// Adds a block after `after`, or first, and puts the caret in it. + pub fn insert( + &mut self, + after: Option, + data: BlockData, + window: &mut Window, + cx: &mut Context, + ) -> u64 { + self.before_change(cx); + let key = self.fresh_key(); + let block = self.build(data, key, window, cx); + let at = after.map_or(0, |after| self.index(after) + 1); + log::info!("block editor: {} block at {at}", block.kind.label()); + let end = block + .fields + .first() + .map(|field| field.read(cx).text().len()); + self.blocks.insert(at, block); + self.after_change(cx); + self.focus_field(key, 0, end, window, cx); + key + } + + /// Changes a block's kind, keeping its first field's text; fields the new kind needs start empty. + pub fn turn_into( + &mut self, + key: u64, + kind: BlockKind, + window: &mut Window, + cx: &mut Context, + ) { + self.before_change(cx); + let ix = self.index(key); + let first = self.blocks[ix] + .fields + .first() + .map(|field| field.read(cx).text().to_string()) + .unwrap_or_default(); + let mut texts = vec![String::new(); kind.fields()]; + if let Some(text) = texts.first_mut() { + *text = first; + } + log::info!("block editor: block {key} turns into {}", kind.label()); + let block = self.build(BlockData { kind, texts }, key, window, cx); + self.blocks[ix] = block; + self.after_change(cx); + self.focus_field(key, 0, None, window, cx); + } + + /// Changes a block's kind in place, as a to-do's box or a toggle's state does. + pub fn set_kind(&mut self, key: u64, kind: BlockKind, cx: &mut Context) { + let ix = self.index(key); + assert!( + kind.accepts(self.blocks[ix].fields.len()), + "set_kind keeps the fields; turn_into changes them" + ); + self.before_change(cx); + self.blocks[ix].kind = kind; + self.after_change(cx); + } + + pub fn remove(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + self.before_change(cx); + let ix = self.index(key); + log::info!("block editor: block {key} removed"); + self.blocks.remove(ix); + self.after_change(cx); + if let Some(previous) = ix.checked_sub(1).and_then(|ix| self.blocks.get(ix)) { + let key = previous.key; + let end = previous + .fields + .first() + .map(|field| field.read(cx).text().len()); + self.focus_field(key, 0, end, window, cx); + } + } + + pub fn duplicate(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + let ix = self.index(key); + let data = self.snapshot(cx).0[ix].1.clone(); + self.insert(Some(key), data, window, cx); + } + + /// Moves the block at `from` to place `to`. + pub fn move_block(&mut self, from: usize, to: usize, cx: &mut Context) { + self.before_change(cx); + log::info!("block editor: block moved from {from} to {to}"); + let block = self.blocks.remove(from); + self.blocks.insert(to.min(self.blocks.len()), block); + self.after_change(cx); + } + + /// Whether `key`'s field `field` holds focus. + pub(crate) fn focused(&self, window: &Window, cx: &App) -> Option<(u64, usize)> { + self.blocks.iter().find_map(|block| { + block + .fields + .iter() + .position(|field| field.focus_handle(cx).is_focused(window)) + .map(|ix| (block.key, ix)) + }) + } +} diff --git a/src/documents/blocks/table.rs b/src/documents/blocks/table.rs new file mode 100644 index 0000000..97b3943 --- /dev/null +++ b/src/documents/blocks/table.rs @@ -0,0 +1,287 @@ +use gpui::{ + AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, Styled, + Window, div, prelude::*, +}; + +use super::{Block, BlockEditor, BlockKind, view::Look}; +use crate::{ + buttons::{ButtonVariant, IconButton}, + primitives::IconName, + theme::ControlSize, +}; + +/// A table's shape: columns, rows, and the cells that fold into their left neighbor. +fn shape(kind: &BlockKind, fields: usize) -> (usize, usize, &[(usize, usize)]) { + let BlockKind::Table { columns, merged } = kind else { + unreachable!("a table block is a table") + }; + (*columns, fields / columns, merged) +} + +/// How many columns the cell at `(row, column)` spans: itself and every cell folded into it. +pub(crate) fn span(merged: &[(usize, usize)], columns: usize, row: usize, column: usize) -> usize { + 1 + (column + 1..columns) + .take_while(|next| merged.contains(&(row, *next))) + .count() +} + +/// Merges after `column` is removed: its own go, the ones right of it move left. +pub(crate) fn without_column(merged: &[(usize, usize)], column: usize) -> Vec<(usize, usize)> { + merged + .iter() + .filter(|(_, at)| *at != column && *at != column + 1) + .map(|&(row, at)| (row, if at > column { at - 1 } else { at })) + .collect() +} + +/// Merges after `row` is removed. +pub(crate) fn without_row(merged: &[(usize, usize)], row: usize) -> Vec<(usize, usize)> { + merged + .iter() + .filter(|(at, _)| *at != row) + .map(|&(at, column)| (if at > row { at - 1 } else { at }, column)) + .collect() +} + +impl BlockEditor { + /// Adds a row of empty cells at the table's foot. + pub(crate) fn add_row(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + let ix = self.index(key); + let (columns, _, _) = shape(&self.blocks[ix].kind, self.blocks[ix].fields.len()); + self.before_change(cx); + for _ in 0..columns { + self.add_field(key, None, window, cx); + } + log::info!("table {key}: a row added"); + self.after_change(cx); + } + + /// Adds a column of empty cells at the table's right. + pub(crate) fn add_column(&mut self, key: u64, window: &mut Window, cx: &mut Context) { + let ix = self.index(key); + let (columns, rows, merged) = shape(&self.blocks[ix].kind, self.blocks[ix].fields.len()); + let merged = merged.to_vec(); + self.before_change(cx); + for row in (0..rows).rev() { + self.add_field(key, Some((row + 1) * columns), window, cx); + } + self.blocks[ix].kind = BlockKind::Table { + columns: columns + 1, + merged, + }; + log::info!("table {key}: a column added"); + self.after_change(cx); + } + + /// Removes the row or column holding cell `cell`; a table keeps a header, a row and a column. + pub(crate) fn remove_line( + &mut self, + key: u64, + cell: usize, + column: bool, + cx: &mut Context, + ) { + let ix = self.index(key); + let (columns, rows, merged) = shape(&self.blocks[ix].kind, self.blocks[ix].fields.len()); + let merged = merged.to_vec(); + let (row, at) = (cell / columns, cell % columns); + if (column && columns == 1) || (!column && rows <= 2) { + log::info!("table {key}: its last line stays"); + return; + } + self.before_change(cx); + let (gone, kind): (Vec, BlockKind) = if column { + ( + (0..rows).map(|row| row * columns + at).collect(), + BlockKind::Table { + columns: columns - 1, + merged: without_column(&merged, at), + }, + ) + } else { + ( + (row * columns..(row + 1) * columns).collect(), + BlockKind::Table { + columns, + merged: without_row(&merged, row), + }, + ) + }; + for ix_field in gone.into_iter().rev() { + self.remove_field(key, ix_field); + } + self.blocks[ix].kind = kind; + log::info!( + "table {key}: {} removed", + if column { "a column" } else { "a row" } + ); + self.after_change(cx); + } + + /// Folds the cell right of `cell` into it, moving its text over; or, when already merged, parts them again. + pub(crate) fn merge_right(&mut self, key: u64, cell: usize, cx: &mut Context) { + let ix = self.index(key); + let (columns, _, merged) = shape(&self.blocks[ix].kind, self.blocks[ix].fields.len()); + let (row, column) = (cell / columns, cell % columns); + let spanned = span(merged, columns, row, column); + if column + spanned >= columns && spanned == 1 { + return; + } + let mut merged = merged.to_vec(); + self.before_change(cx); + if spanned > 1 { + merged + .retain(|(at_row, at)| !(*at_row == row && *at > column && *at < column + spanned)); + log::info!("table {key}: cell {cell} parted"); + } else { + let right = self.blocks[ix].fields[cell + 1].read(cx).text().to_string(); + if !right.is_empty() { + self.blocks[ix].fields[cell].update(cx, |field, cx| { + let end = field.text().len(); + field.select(end..end, cx); + field.insert(&format!(" {right}"), cx); + }); + self.blocks[ix].fields[cell + 1].update(cx, |field, cx| field.set_text("", cx)); + } + merged.push((row, column + 1)); + log::info!("table {key}: cell {cell} merged right"); + } + self.blocks[ix].kind = BlockKind::Table { columns, merged }; + self.after_change(cx); + } +} + +/// The table: a header row, then rows; cells merged across span their neighbors; tools above while a cell has focus. +pub(super) fn table( + editor: &BlockEditor, + block: &Block, + look: &Look, + window: &mut Window, + cx: &mut Context, +) -> AnyElement { + let (columns, rows, merged) = shape(&block.kind, block.fields.len()); + let merged = merged.to_vec(); + let (key, id) = (block.key, BlockEditor::id(cx)); + let focused = editor + .focused(window, cx) + .filter(|(at, _)| *at == key) + .map(|(_, cell)| cell); + let colors = &look.colors; + let tool = |name: &'static str, + icon: IconName, + run: fn(&mut BlockEditor, u64, usize, &mut Window, &mut Context), + cell: usize, + cx: &mut Context| { + IconButton::new((id.clone(), format!("table-{key}-{name}")), icon) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .tooltip(name) + .on_click(cx.listener(move |editor, _, window, cx| run(editor, key, cell, window, cx))) + }; + let tools = focused.map(|cell| { + div() + .flex() + .gap_0p5() + .child(tool( + "Add row", + IconName::Rows2, + |editor, key, _, window, cx| editor.add_row(key, window, cx), + cell, + cx, + )) + .child(tool( + "Add column", + IconName::Columns2, + |editor, key, _, window, cx| editor.add_column(key, window, cx), + cell, + cx, + )) + .child(tool( + "Remove row", + IconName::Minus, + |editor, key, cell, _, cx| editor.remove_line(key, cell, false, cx), + cell, + cx, + )) + .child(tool( + "Remove column", + IconName::X, + |editor, key, cell, _, cx| editor.remove_line(key, cell, true, cx), + cell, + cx, + )) + .child(tool( + "Merge or part cells", + IconName::Merge, + |editor, key, cell, _, cx| editor.merge_right(key, cell, cx), + cell, + cx, + )) + }); + let mut grid = div() + .flex() + .flex_col() + .rounded(look.radius) + .border_1() + .border_color(colors.border); + for row in 0..rows { + let mut line = div().flex().when(row > 0, |line| { + line.border_t_1().border_color(colors.border) + }); + if row == 0 { + line = line.bg(colors.hover).font_weight(FontWeight::MEDIUM); + } + for column in 0..columns { + if merged.contains(&(row, column)) { + continue; + } + let cell = row * columns + column; + let wide = span(&merged, columns, row, column); + line = line.child( + div() + .flex_grow() + .flex_basis(gpui::relative(wide as f32 / columns as f32)) + .min_w_0() + .px_2() + .py_1() + .when(column > 0, |cell| { + cell.border_l_1().border_color(colors.border) + }) + .child(editor.field_view(block, cell, window, cx)), + ); + } + grid = grid.child(line); + } + div() + .id((id.clone(), format!("table-{key}"))) + .flex() + .flex_col() + .gap_1() + .children(tools) + .child(grid) + .into_any_element() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_merged_cell_spans_what_folds_into_it() { + let merged = [(1, 1), (1, 2)]; + assert_eq!(span(&merged, 4, 1, 0), 3); + assert_eq!(span(&merged, 4, 0, 0), 1); + assert_eq!(span(&merged, 4, 1, 3), 1); + } + + #[test] + fn removing_a_line_moves_the_merges_past_it() { + let merged = [(0, 1), (1, 3), (2, 2)]; + assert_eq!( + without_column(&merged, 1), + [(1, 2)], + "a cell folded into the removed column parts" + ); + assert_eq!(without_row(&merged, 1), [(0, 1), (1, 2)]); + } +} diff --git a/src/documents/blocks/view.rs b/src/documents/blocks/view.rs new file mode 100644 index 0000000..b4b8cb8 --- /dev/null +++ b/src/documents/blocks/view.rs @@ -0,0 +1,288 @@ +use std::rc::Rc; + +use gpui::{ + AnyElement, App, Context, ElementId, InteractiveElement, IntoElement, MouseButton, + ParentElement, Rems, Render, SharedString, Styled, Window, div, prelude::*, +}; + +use super::{Block, BlockData, BlockEditor, BlockKind, matching}; +use crate::{ + buttons::{ButtonVariant, IconButton}, + documents::suggest::{Offer, offers, replace_trigger}, + forms::{Backspace, Choice, Down, Enter, Pick, Redo, Submit, Undo, Up}, + menus::{Menu, MenuItem, OverflowMenu}, + motion::Reorder, + primitives::IconName, + theme::{ActiveTheme, ControlSize, Palette, Radius, TextSize}, +}; + +/// Theme values a block reads, taken before its fields borrow the context. +pub(crate) struct Look { + pub colors: Palette, + pub mono: SharedString, + pub radius: Rems, + pub small: Rems, + pub tiny: Rems, + pub headings: [Rems; 3], +} + +impl Look { + fn new(cx: &App) -> Self { + let theme = cx.theme(); + Self { + colors: theme.colors.clone(), + mono: theme.mono_family.clone(), + radius: theme.radius(Radius::Md), + small: theme.text_size(TextSize::Sm), + tiny: theme.text_size(TextSize::Xs), + headings: [TextSize::Xxl, TextSize::Xl, TextSize::Lg].map(|size| theme.text_size(size)), + } + } +} + +impl BlockEditor { + pub(crate) fn id(cx: &Context) -> ElementId { + ("ely-blocks", cx.entity_id()).into() + } + + /// A slash pick: an emptied block becomes the kind; one with text gets the kind below it. + fn pick_kind( + &mut self, + key: u64, + kind: BlockKind, + window: &mut Window, + cx: &mut Context, + ) { + let ix = self.index(key); + let empty = self.blocks[ix] + .fields + .first() + .is_some_and(|field| field.read(cx).is_empty()); + let data = BlockData { + texts: vec![String::new(); kind.fields()], + kind: kind.clone(), + }; + let placed = if empty { + self.turn_into(key, kind.clone(), window, cx); + key + } else { + self.insert(Some(key), data, window, cx) + }; + if kind == BlockKind::Divider { + self.insert( + Some(placed), + BlockData::new(BlockKind::Paragraph, [""]), + window, + cx, + ); + } + } + + /// A text field with its keys and suggests: `/` in prose opens the kinds. + pub(crate) fn field_view( + &self, + block: &Block, + ix: usize, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let (key, field) = (block.key, block.fields[ix].clone()); + let entity = cx.entity(); + let keys = div() + .w_full() + .capture_action(cx.listener(move |editor, _: &Enter, window, cx| { + if editor.on_enter(key, ix, window, cx) { + cx.stop_propagation(); + } + })) + .capture_action(cx.listener(move |editor, _: &Backspace, window, cx| { + if editor.on_backspace(key, ix, window, cx) { + cx.stop_propagation(); + } + })) + .capture_action(cx.listener(move |editor, _: &Up, window, cx| { + if editor.on_vertical(key, ix, false, window, cx) { + cx.stop_propagation(); + } + })) + .capture_action(cx.listener(move |editor, _: &Down, window, cx| { + if editor.on_vertical(key, ix, true, window, cx) { + cx.stop_propagation(); + } + })) + .capture_action(cx.listener(move |editor, _: &Submit, window, cx| { + cx.stop_propagation(); + editor.on_submit(key, window, cx); + })) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .child(field.clone()); + let slash = move |query: &str, at: usize, caret: usize| -> (Vec, Pick) { + let kinds = matching(query); + let rows = kinds + .iter() + .map(|kind| Choice::new(kind.label(), kind.label()).icon(kind.icon())) + .collect(); + let (entity, field) = (entity.clone(), field.clone()); + let pick: Pick = Rc::new(move |pick: usize, window: &mut Window, cx: &mut App| { + replace_trigger(&field, at, caret, "", cx); + let kind = kinds[pick].clone(); + entity.update(cx, |editor, cx| editor.pick_kind(key, kind, window, cx)); + }); + (rows, pick) + }; + let extra: Option<(char, Offer)> = + (block.kind.is_prose() && ix == 0).then_some(('/', &slash)); + let id = (Self::id(cx), format!("offers-{key}-{ix}")).into(); + offers(id, &block.fields[ix], &self.people, extra, cx) + .wrap(keys, window, cx) + .into_any_element() + } + + /// The handle's menu: turn into another kind, duplicate or delete. + fn menu(&self, key: u64, cx: &mut Context) -> Menu { + let entity = cx.entity(); + let turn = super::kind::offered() + .into_iter() + .fold(Menu::new(), |menu, kind| { + let entity = entity.clone(); + menu.item(MenuItem::new(kind.label()).icon(kind.icon()).on_click( + move |window, cx| { + let kind = kind.clone(); + entity.update(cx, |editor, cx| editor.turn_into(key, kind, window, cx)); + }, + )) + }); + let (copy, gone) = (entity.clone(), entity); + Menu::new() + .item(MenuItem::submenu("Turn into", turn).icon(IconName::RefreshCw)) + .item( + MenuItem::new("Duplicate") + .icon(IconName::Copy) + .on_click(move |window, cx| { + copy.update(cx, |editor, cx| editor.duplicate(key, window, cx)); + }), + ) + .separator() + .item( + MenuItem::new("Delete") + .icon(IconName::Trash2) + .on_click(move |window, cx| { + gone.update(cx, |editor, cx| editor.remove(key, window, cx)); + }), + ) + } +} + +impl Render for BlockEditor { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let id = Self::id(cx); + let look = Look::new(cx); + let colors = look.colors.clone(); + let focused = self.focused(window, cx).map(|(key, _)| key); + let mut number = 0; + let mut rows: Vec<(SharedString, AnyElement)> = Vec::new(); + for ix in 0..self.blocks.len() { + let block = &self.blocks[ix]; + number = if block.kind == BlockKind::Numbered { + number + 1 + } else { + 0 + }; + let key = block.key; + let group = SharedString::from(format!("block-{key}")); + let entity = cx.entity(); + let plus = IconButton::new((id.clone(), format!("plus-{key}")), IconName::Plus) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .tooltip("Add a block below") + .on_click(move |_, window, cx| { + entity.update(cx, |editor, cx| { + editor.insert( + Some(key), + BlockData::new(BlockKind::Paragraph, ["/"]), + window, + cx, + ); + }) + }); + let handle = + OverflowMenu::new((id.clone(), format!("handle-{key}")), self.menu(key, cx)) + .icon(IconName::GripVertical) + .tooltip("Drag to move, press for more"); + let content = self.content(&self.blocks[ix], number, &look, window, cx); + let row = div() + .id((id.clone(), format!("row-{key}"))) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |editor, _, window, cx| { + let end = editor.blocks[editor.index(key)] + .fields + .first() + .map(|field| field.read(cx).text().len()); + if end.is_some() { + editor.focus_field(key, 0, end, window, cx); + } + }), + ) + .group(group.clone()) + .relative() + .pl_16() + .py_0p5() + .child(content) + .child( + div() + .absolute() + .top_0() + .left_0() + .flex() + .opacity(if focused == Some(key) { 1.0 } else { 0.0 }) + .group_hover(group, |gutter| gutter.opacity(1.0)) + .child(plus) + .child(handle), + ); + rows.push((format!("{key}").into(), row.into_any_element())); + } + let entity = cx.entity(); + let list = rows.into_iter().fold( + Reorder::new((id.clone(), "blocks")).on_reorder(move |from, to, _, cx| { + entity.update(cx, |editor, cx| editor.move_block(from, to, cx)) + }), + |list, (key, row)| list.row(key, row), + ); + let start = cx.entity(); + div() + .id(id.clone()) + .w_full() + .flex() + .flex_col() + .capture_action(cx.listener(|editor, _: &Undo, window, cx| { + cx.stop_propagation(); + editor.undo(window, cx); + })) + .capture_action(cx.listener(|editor, _: &Redo, window, cx| { + cx.stop_propagation(); + editor.redo(window, cx); + })) + .child(list) + .child( + div() + .id((id, "end")) + .min_h_8() + .cursor_text() + .text_color(colors.fg_subtle) + .when(self.blocks.is_empty(), |end| end.child("Press to write")) + .on_mouse_down(MouseButton::Left, move |_, window, cx| { + window.prevent_default(); + start.update(cx, |editor, cx| { + let last = editor.blocks.last().map(|block| block.key); + editor.insert( + last, + BlockData::new(BlockKind::Paragraph, [""]), + window, + cx, + ); + }); + }), + ) + } +} diff --git a/src/documents/counts.rs b/src/documents/counts.rs new file mode 100644 index 0000000..b502ee8 --- /dev/null +++ b/src/documents/counts.rs @@ -0,0 +1,85 @@ +use gpui::{App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div}; +use unicode_segmentation::UnicodeSegmentation; + +use super::markdown::{reading_minutes, words}; +use crate::{ + theme::ActiveTheme, + typography::{format::plural, tabular}, +}; + +/// Share of a limit past which the count warns. +const NEAR: f32 = 0.9; + +/// Words and characters in a text, as one quiet line; against a limit, characters warn near it and turn to danger past it. +#[derive(IntoElement)] +pub struct WordCount { + text: SharedString, + limit: Option, +} + +impl WordCount { + pub fn new(text: impl Into) -> Self { + Self { + text: text.into(), + limit: None, + } + } + + /// Counts characters against `limit`. + pub fn limit(mut self, limit: usize) -> Self { + assert!(limit > 0, "a character limit of zero allows nothing"); + self.limit = Some(limit); + self + } +} + +impl RenderOnce for WordCount { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let colors = &cx.theme().colors; + let characters = self.text.graphemes(true).count(); + let words = plural(words(&self.text) as u64, "word", "words"); + let (characters, color) = match self.limit { + Some(limit) => ( + format!("{characters}/{limit}"), + if characters > limit { + colors.danger + } else if characters as f32 >= limit as f32 * NEAR { + colors.warning + } else { + colors.fg_subtle + }, + ), + None => ( + plural(characters as u64, "character", "characters"), + colors.fg_subtle, + ), + }; + tabular(div().flex().gap_1().text_color(colors.fg_subtle)) + .child(words) + .child("·") + .child(div().text_color(color).child(characters)) + } +} + +/// How long a text takes to read, at an adult's pace. +#[derive(IntoElement)] +pub struct ReadingTime { + text: SharedString, +} + +impl ReadingTime { + pub fn new(text: impl Into) -> Self { + Self { text: text.into() } + } +} + +impl RenderOnce for ReadingTime { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let minutes = reading_minutes(words(&self.text)); + tabular(div().text_color(cx.theme().colors.fg_subtle)).child(if minutes == 0 { + "Nothing to read yet".to_string() + } else { + format!("{minutes} min read") + }) + } +} diff --git a/src/documents/format.rs b/src/documents/format.rs new file mode 100644 index 0000000..82340b9 --- /dev/null +++ b/src/documents/format.rs @@ -0,0 +1,233 @@ +use std::ops::Range; + +use gpui::{App, Entity}; + +use super::markdown::{Mark, marks}; +use crate::forms::TextInput; + +/// A format the toolbars apply to markdown. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Format { + Bold, + Italic, + Strike, + Code, + Link, + Heading(u8), + Quote, + Bullet, + Numbered, + Task, +} + +impl Format { + /// The marker an inline format wraps its text in. + fn marker(self) -> Option<&'static str> { + match self { + Self::Bold => Some("**"), + Self::Italic => Some("*"), + Self::Strike => Some("~~"), + Self::Code => Some("`"), + _ => None, + } + } + + /// The prefix a line format starts its lines with; numbered lines count up from one. + fn prefix(self, nth: usize) -> String { + match self { + Self::Heading(level) => format!("{} ", "#".repeat(level.clamp(1, 6) as usize)), + Self::Quote => "> ".into(), + Self::Bullet => "- ".into(), + Self::Numbered => format!("{}. ", nth + 1), + Self::Task => "- [ ] ".into(), + _ => unreachable!("an inline format has no prefix"), + } + } +} + +/// A numbered line's prefix, such as `12. `, in bytes; none for other lines. +fn number_prefix(line: &str) -> usize { + let digits = line.chars().take_while(char::is_ascii_digit).count(); + if digits > 0 && line[digits..].starts_with(". ") { + digits + 2 + } else { + 0 + } +} + +/// Whether `line` already starts as `format` writes it. +fn has(format: Format, line: &str) -> bool { + match format { + Format::Task => line.starts_with("- [ ] ") || line.starts_with("- [x] "), + Format::Bullet => { + (line.starts_with("- ") || line.starts_with("* ")) && !has(Format::Task, line) + } + Format::Numbered => number_prefix(line) > 0, + _ => line.starts_with(&format.prefix(0)), + } +} + +/// A line's own prefix: heading hashes, quote, bullet, number or task box, with its space. +fn line_prefix(line: &str) -> usize { + let hashes = line.chars().take_while(|ch| *ch == '#').count(); + if (1..=6).contains(&hashes) && line[hashes..].starts_with(' ') { + return hashes + 1; + } + for mark in ["- [ ] ", "- [x] ", "> ", "- ", "* "] { + if line.starts_with(mark) { + return mark.len(); + } + } + number_prefix(line) +} + +/// `text` with `format` applied over `selection`, and where the selection lands. An inline format wraps the selection, or unwraps it when already wrapped; an empty one leaves the caret between its markers. A line format prefixes each touched line, or takes the prefix off when every line has it. +pub fn apply(text: &str, selection: Range, format: Format) -> (String, Range) { + assert!( + selection.start <= selection.end && selection.end <= text.len(), + "the selection {selection:?} lies outside {} bytes", + text.len() + ); + if format == Format::Link { + let label = &text[selection.clone()]; + let linked = format!("{}[{label}]()", &text[..selection.start]); + let caret = linked.len() - 1; + return (format!("{linked}{}", &text[selection.end..]), caret..caret); + } + if let Some(marker) = format.marker() { + let (before, after) = (&text[..selection.start], &text[selection.end..]); + let inner = &text[selection.clone()]; + if before.ends_with(marker) && after.starts_with(marker) { + let start = selection.start - marker.len(); + let unwrapped = format!("{}{inner}{}", &before[..start], &after[marker.len()..]); + return (unwrapped, start..selection.end - marker.len()); + } + let shift = marker.len(); + let wrapped = format!("{before}{marker}{inner}{marker}{after}"); + return (wrapped, selection.start + shift..selection.end + shift); + } + let first = text[..selection.start].rfind('\n').map_or(0, |at| at + 1); + let last = text[selection.end..] + .find('\n') + .map_or(text.len(), |at| selection.end + at); + let lines: Vec<&str> = text[first..last].split('\n').collect(); + let every = lines.iter().all(|line| has(format, line)); + let changed: Vec = lines + .iter() + .enumerate() + .map(|(nth, line)| { + let bare = &line[line_prefix(line)..]; + if every { + bare.to_string() + } else { + format!("{}{bare}", format.prefix(nth)) + } + }) + .collect(); + let block = changed.join("\n"); + let out = format!("{}{block}{}", &text[..first], &text[last..]); + (out, first..first + block.len()) +} + +/// The one stretch where `new` differs from `old`: the range it replaces and what replaces it. +fn changed<'a>(old: &str, new: &'a str) -> (Range, &'a str) { + let head: usize = old + .chars() + .zip(new.chars()) + .take_while(|(was, now)| was == now) + .map(|(was, _)| was.len_utf8()) + .sum(); + let tail: usize = old[head..] + .chars() + .rev() + .zip(new[head..].chars().rev()) + .take_while(|(was, now)| was == now) + .map(|(was, _)| was.len_utf8()) + .sum(); + (head..old.len() - tail, &new[head..new.len() - tail]) +} + +/// Applies `format` to the field's selection as one undo step, then selects what it formatted. +pub fn format(field: &Entity, format: Format, cx: &mut App) { + field.update(cx, |input, cx| { + let text = input.text().to_string(); + let (next, selection) = apply(&text, input.selection(), format); + let (range, inserted) = changed(&text, &next); + log::info!("format {format:?}: {range:?} becomes {inserted:?}"); + input.select(range, cx); + input.insert(inserted, cx); + input.select(selection, cx); + }); +} + +/// The formats that hold all of `selection`, for a toolbar to show pressed. +pub fn active(text: &str, selection: Range) -> Vec { + let mut on: Vec = marks(text) + .into_iter() + .filter(|(range, _)| range.start <= selection.start && selection.end <= range.end) + .filter_map(|(_, mark)| match mark { + Mark::Strong => Some(Format::Bold), + Mark::Emphasis => Some(Format::Italic), + Mark::Strike => Some(Format::Strike), + Mark::Code => Some(Format::Code), + Mark::Link => Some(Format::Link), + Mark::Heading(level) => Some(Format::Heading(level)), + Mark::Quote => Some(Format::Quote), + Mark::Syntax => None, + }) + .collect(); + let line = &text[text[..selection.start].rfind('\n').map_or(0, |at| at + 1)..]; + on.extend( + [Format::Task, Format::Bullet, Format::Numbered] + .into_iter() + .find(|list| has(*list, line)), + ); + on +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inline_formats_wrap_and_unwrap() { + let (bold, selection) = apply("a lift b", 2..6, Format::Bold); + assert_eq!((bold.as_str(), selection.clone()), ("a **lift** b", 4..8)); + assert_eq!( + apply(&bold, selection, Format::Bold), + ("a lift b".into(), 2..6) + ); + assert_eq!(apply("ab", 1..1, Format::Code), ("a``b".into(), 2..2)); + assert_eq!( + apply("see docs", 4..8, Format::Link), + ("see [docs]()".into(), 11..11) + ); + } + + #[test] + fn line_formats_prefix_every_touched_line_or_take_it_off() { + let text = "one\ntwo\nthree"; + let (listed, selection) = apply(text, 1..6, Format::Numbered); + assert_eq!(listed, "1. one\n2. two\nthree"); + assert_eq!(apply(&listed, selection, Format::Numbered).0, text); + assert_eq!(apply("## Old", 3..3, Format::Heading(1)).0, "# Old"); + assert_eq!(apply("- item", 2..2, Format::Task).0, "- [ ] item"); + assert_eq!(apply("- [x] done", 2..2, Format::Task).0, "done"); + assert_eq!(apply("- [ ] task", 2..2, Format::Bullet).0, "- task"); + } + + #[test] + fn a_change_is_the_stretch_between_what_stayed() { + assert_eq!(changed("a lift b", "a **lift** b"), (2..6, "**lift**")); + assert_eq!(changed("same", "same"), (4..4, "")); + assert_eq!(changed("héllo", "hello"), (1..3, "e")); + } + + #[test] + fn pressed_formats_hold_the_selection() { + let text = "- [ ] a **bold** word"; + assert_eq!(active(text, 10..12), [Format::Bold, Format::Task]); + assert_eq!(active("## Lift", 4..4), [Format::Heading(2)]); + assert_eq!(active("12. twelve", 5..5), [Format::Numbered]); + } +} diff --git a/src/documents/link.rs b/src/documents/link.rs new file mode 100644 index 0000000..0b1edc8 --- /dev/null +++ b/src/documents/link.rs @@ -0,0 +1,233 @@ +use std::{ops::Range, rc::Rc}; + +use gpui::{ + App, AppContext as _, Context, ElementId, Entity, Focusable, InteractiveElement, IntoElement, + ParentElement, RenderOnce, Styled, Window, div, prelude::*, +}; + +use super::markdown::link_at; +use crate::{ + buttons::{Button, ButtonVariant, IconButton}, + forms::{Enter, Input, Run, TextInput, float, is_url, surface}, + primitives::{FocusScope, IconName, give_back, take_focus}, + theme::{ActiveTheme, ControlSize}, +}; + +/// What the editor edits: a link's range and words, or the selection that becomes one. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct Linking { + pub range: Range, + pub label: String, + /// The address, when the range is already a link. + pub address: Option, +} + +impl Linking { + /// The link around the selection, or the selection itself. + pub fn at(text: &str, selection: Range) -> Self { + match link_at(text, selection.start) { + Some((range, label, address)) if selection.end <= range.end => Self { + range, + label, + address: Some(address), + }, + _ => Self { + label: text[selection.clone()].to_string(), + range: selection, + address: None, + }, + } + } + + /// The link written in markdown for `address`; with no words of its own it reads as its address. + pub fn written(&self, address: &str) -> String { + let label = if self.label.is_empty() { + address + } else { + &self.label + }; + format!("[{label}]({address})") + } +} + +/// The address field and whether it took focus yet. +struct Editing { + address: Entity, + seeded: bool, +} + +/// Replaces `range` of the field's text with `text` as one undo step, and closes. +fn write(field: &Entity, range: Range, text: &str, cx: &mut App) { + field.update(cx, |input, cx| { + input.select(range, cx); + input.insert(text, cx); + }); +} + +/// Edits the link at the caret, or links the selection, in a panel under it: the address is checked as it is typed and applied with Enter; the link can be removed or opened from the same place. Escape or a press outside closes it, and focus returns to the text. Render it only while open. +#[derive(IntoElement)] +pub struct LinkEditor { + id: ElementId, + field: Entity, + on_close: Run, +} + +impl LinkEditor { + pub fn new( + id: impl Into, + field: &Entity, + on_close: impl Fn(&mut Window, &mut App) + 'static, + ) -> Self { + Self { + id: id.into(), + field: field.clone(), + on_close: Rc::new(on_close), + } + } +} + +impl RenderOnce for LinkEditor { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let input = self.field.read(cx); + let linking = Linking::at(input.text(), input.selection()); + let anchor = input.bounds_for(linking.range.start); + let takeover = take_focus((self.id.clone(), "takeover"), window, cx); + let editing = window.use_keyed_state( + (self.id.clone(), "editing"), + cx, + |window, cx: &mut Context| Editing { + address: cx.new(|cx| TextInput::new(window, cx).placeholder("https://")), + seeded: false, + }, + ); + let address = editing.read(cx).address.clone(); + if !editing.read(cx).seeded { + log::info!("link editor: open over {:?}", linking.range); + let written = linking.address.clone().unwrap_or_default(); + address.update(cx, |field, cx| field.set_text(written, cx)); + window.focus(&address.focus_handle(cx)); + editing.update(cx, |editing, _| editing.seeded = true); + } + let focus = takeover.read(cx).focus.clone(); + let close: Run = { + let (takeover, on_close) = (takeover.clone(), self.on_close.clone()); + Rc::new(move |window, cx| { + log::info!("link editor: closed"); + give_back(&takeover, window, cx); + on_close(window, cx); + }) + }; + let typed = address.read(cx).text().trim().to_string(); + let valid = is_url(&typed); + let apply: Run = { + let (field, linking, close) = (self.field.clone(), linking.clone(), close.clone()); + let typed = typed.clone(); + Rc::new(move |window, cx| { + if !is_url(&typed) { + return; + } + log::info!("link editor: {typed} over {:?}", linking.range); + write(&field, linking.range.clone(), &linking.written(&typed), cx); + close(window, cx); + }) + }; + let remove: Option = linking.address.as_ref().map(|_| { + let (field, linking, close) = (self.field.clone(), linking.clone(), close.clone()); + Rc::new(move |window: &mut Window, cx: &mut App| { + log::info!("link editor: unlinked {:?}", linking.range); + write(&field, linking.range.clone(), &linking.label, cx); + close(window, cx); + }) as Run + }); + let Some(anchor) = anchor else { + return div().id(self.id); + }; + let (enter, escape, out) = (apply.clone(), close.clone(), close.clone()); + let press = apply.clone(); + let opened = linking.address.clone(); + let panel = div() + .id((self.id.clone(), "panel")) + .occlude() + .capture_action(move |_: &Enter, window, cx| { + cx.stop_propagation(); + enter(window, cx); + }) + .on_key_down(move |event, window, cx| { + if event.keystroke.key == "escape" { + cx.stop_propagation(); + escape(window, cx); + } + }) + .on_mouse_down_out(move |_, window, cx| out(window, cx)) + .child( + FocusScope::new(&focus).trap().child( + surface((self.id.clone(), "surface"), cx) + .p_2() + .flex() + .items_center() + .gap_2() + .child( + div().w(cx.theme().menu_width()).child( + Input::new(&address) + .size(ControlSize::Sm) + .invalid(!typed.is_empty() && !valid), + ), + ) + .child( + Button::new((self.id.clone(), "apply"), "Apply") + .variant(ButtonVariant::Primary) + .size(ControlSize::Sm) + .disabled(!valid) + .on_click(move |_, window, cx| press(window, cx)), + ) + .when_some(opened, |row, url| { + row.child( + IconButton::new((self.id.clone(), "open"), IconName::ExternalLink) + .size(ControlSize::Sm) + .tooltip("Open link") + .on_click(move |_, _, cx| cx.open_url(&url)), + ) + }) + .when_some(remove, |row, remove| { + row.child( + IconButton::new((self.id.clone(), "remove"), IconName::Trash2) + .size(ControlSize::Sm) + .tooltip("Remove link") + .on_click(move |_, window, cx| remove(window, cx)), + ) + }), + ), + ); + div().id(self.id).child(float(anchor, 1, panel, window, cx)) + } +} + +#[cfg(test)] +mod tests { + use super::Linking; + + #[test] + fn a_link_is_edited_whole_and_a_selection_becomes_one() { + let text = "see [the docs](https://ely.dev) here"; + let inside = Linking::at(text, 8..8); + assert_eq!( + (inside.range, inside.address.as_deref()), + (4..31, Some("https://ely.dev")) + ); + let plain = Linking::at(text, 32..36); + assert_eq!( + ( + plain.range.clone(), + plain.label.as_str(), + plain.address.clone() + ), + (32..36, "here", None) + ); + assert_eq!(plain.written("https://x.dev"), "[here](https://x.dev)"); + let bare = Linking::at(text, 0..0); + assert_eq!( + bare.written("https://x.dev"), + "[https://x.dev](https://x.dev)" + ); + } +} diff --git a/src/documents/markdown.rs b/src/documents/markdown.rs new file mode 100644 index 0000000..7f84d1c --- /dev/null +++ b/src/documents/markdown.rs @@ -0,0 +1,279 @@ +use std::ops::Range; + +use gpui::{App, FontWeight}; +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{forms::Highlight, theme::ActiveTheme}; + +/// The markdown this crate reads: GitHub's tables, strikethrough and task lists, footnotes, and math between dollars. +pub(crate) fn options() -> Options { + Options::ENABLE_TABLES + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_TASKLISTS + | Options::ENABLE_FOOTNOTES + | Options::ENABLE_MATH +} + +/// How a stretch of markdown prints while it is edited. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mark { + Strong, + Emphasis, + Strike, + Code, + Link, + Heading(u8), + Quote, + /// The characters that write the marks: asterisks, backticks, brackets, hashes. + Syntax, +} + +/// The run of `ch` that starts `text`, in bytes. +fn leading(text: &str, ch: char) -> usize { + text.chars() + .take_while(|found| *found == ch) + .map(char::len_utf8) + .sum() +} + +/// The marks over `text`'s bytes: styles, code, links, headings and quotes, and the syntax that writes each. +pub fn marks(text: &str) -> Vec<(Range, Mark)> { + let mut out = Vec::new(); + let mut links: Vec<(Range, Option>)> = Vec::new(); + for (event, range) in Parser::new_ext(text, options()).into_offset_iter() { + let span = &text[range.clone()]; + match event { + Event::Start(Tag::Strong | Tag::Emphasis | Tag::Strikethrough) => { + let mark = match event { + Event::Start(Tag::Strong) => Mark::Strong, + Event::Start(Tag::Emphasis) => Mark::Emphasis, + _ => Mark::Strike, + }; + let first = span.chars().next().expect("a styled span has its marker"); + let edge = leading(span, first).min(span.len() / 2); + let edge = match mark { + Mark::Emphasis => edge.min(first.len_utf8()), + _ => edge, + }; + out.push((range.clone(), mark)); + out.push((range.start..range.start + edge, Mark::Syntax)); + out.push((range.end - edge..range.end, Mark::Syntax)); + } + Event::Code(_) => { + let ticks = leading(span, '`'); + out.push((range.clone(), Mark::Code)); + out.push((range.start..range.start + ticks, Mark::Syntax)); + out.push((range.end - ticks..range.end, Mark::Syntax)); + } + Event::Start(Tag::Link { .. }) => links.push((range, None)), + Event::Text(_) if !links.is_empty() => { + let (_, inner) = links.last_mut().expect("inside a link"); + *inner = Some(match inner.take() { + Some(inner) => inner.start..range.end, + None => range, + }); + } + Event::End(TagEnd::Link) => { + let (whole, inner) = links.pop().expect("a link ends after it starts"); + out.push((whole.clone(), Mark::Link)); + if let Some(inner) = inner { + out.push((whole.start..inner.start, Mark::Syntax)); + out.push((inner.end..whole.end, Mark::Syntax)); + } + } + Event::Start(Tag::Heading { level, .. }) => { + let hashes = leading(span, '#'); + out.push((range.clone(), Mark::Heading(level as u8))); + if hashes > 0 { + let spaced = hashes + leading(&span[hashes..], ' '); + out.push((range.start..range.start + spaced, Mark::Syntax)); + } + } + Event::Start(Tag::BlockQuote(_)) => { + out.push((range.clone(), Mark::Quote)); + let mut at = range.start; + for line in span.split_inclusive('\n') { + let marker = line.len() - line.trim_start_matches(['>', ' ']).len(); + out.push((at..at + marker, Mark::Syntax)); + at += line.len(); + } + } + _ => {} + } + } + out.retain(|(range, _)| !range.is_empty()); + out +} + +/// Markdown styled as it is typed: strong and headings heavier, emphasis slanted, strikes struck, code washed, links in their tone; the syntax that writes them recedes. +pub fn markdown_highlights(text: &str, cx: &App) -> Vec<(Range, Highlight)> { + let colors = &cx.theme().colors; + let plain = Highlight::new(colors.fg); + marks(text) + .into_iter() + .map(|(range, mark)| { + let range = match mark { + Mark::Strike => { + let span = &text[range.clone()]; + let edge = span.len() - span.trim_start_matches('~').len(); + range.start + edge..range.end - edge + } + _ => range, + }; + let highlight = match mark { + Mark::Strong | Mark::Heading(_) => Highlight { + weight: Some(FontWeight::SEMIBOLD), + ..plain + }, + Mark::Emphasis => Highlight { + italic: true, + ..plain + }, + Mark::Strike => Highlight { + strike: true, + ..Highlight::new(colors.fg_muted) + }, + Mark::Code => Highlight { + background: Some(colors.sunken), + ..plain + }, + Mark::Link => Highlight::new(colors.link), + Mark::Quote => Highlight::new(colors.fg_muted), + Mark::Syntax => Highlight::new(colors.fg_subtle), + }; + (range, highlight) + }) + .collect() +} + +/// A link written `[text](address)` around byte `at`: its range, its text and its address. +pub fn link_at(text: &str, at: usize) -> Option<(Range, String, String)> { + Parser::new_ext(text, options()) + .into_offset_iter() + .find_map(|(event, range)| match event { + Event::Start(Tag::Link { dest_url, .. }) + if range.start <= at && at <= range.end && text[range.clone()].starts_with('[') => + { + let written = &text[range.clone()]; + let label = &written[1..written.rfind("](")?]; + Some((range, label.to_string(), dest_url.to_string())) + } + _ => None, + }) +} + +/// The document's top-level blocks, as ranges of its source. +pub fn block_ranges(text: &str) -> Vec> { + let mut depth = 0usize; + let mut out = Vec::new(); + for (event, range) in Parser::new_ext(text, options()).into_offset_iter() { + match event { + Event::Start(_) => { + if depth == 0 { + out.push(range); + } + depth += 1; + } + Event::End(_) => depth -= 1, + Event::Rule if depth == 0 => out.push(range), + _ => {} + } + } + out.into_iter() + .map(|range| { + let kept = text[range.clone()].trim_end_matches(['\n', '\r']).len(); + range.start..range.start + kept + }) + .collect() +} + +/// Words in `text` as a reader counts them; each ideograph counts as one. +pub fn words(text: &str) -> usize { + text.unicode_words().count() +} + +/// Words an adult reads in a minute, on average. +const PACE: usize = 238; + +/// Minutes to read `words`, at least one when there are any. +pub fn reading_minutes(words: usize) -> usize { + words.div_ceil(PACE) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn found(text: &str) -> Vec<(&str, Mark)> { + marks(text) + .into_iter() + .map(|(range, mark)| (&text[range], mark)) + .collect() + } + + #[test] + fn styles_carry_their_markers_as_syntax() { + use Mark::*; + assert_eq!( + found("a **bold** and *it* ~~no~~"), + [ + ("**bold**", Strong), + ("**", Syntax), + ("**", Syntax), + ("*it*", Emphasis), + ("*", Syntax), + ("*", Syntax), + ("~~no~~", Strike), + ("~~", Syntax), + ("~~", Syntax), + ] + ); + } + + #[test] + fn code_links_and_headings_mark_their_syntax() { + use Mark::*; + assert_eq!(found("`x`"), [("`x`", Code), ("`", Syntax), ("`", Syntax)]); + assert_eq!( + found("see [the docs](https://ely.dev)"), + [ + ("[the docs](https://ely.dev)", Link), + ("[", Syntax), + ("](https://ely.dev)", Syntax) + ] + ); + assert_eq!(found("## Lift"), [("## Lift", Heading(2)), ("## ", Syntax)]); + } + + #[test] + fn a_link_gives_its_range_text_and_address() { + let text = "see [the docs](https://ely.dev) or "; + assert_eq!( + link_at(text, 6), + Some((4..31, "the docs".to_string(), "https://ely.dev".to_string())) + ); + assert_eq!(link_at(text, 1), None); + assert_eq!(link_at(text, 40), None, "an autolink has no text to edit"); + } + + #[test] + fn blocks_are_the_top_level_stretches() { + let text = "# Lift\n\nBlend **it**.\n\n- a\n- b\n\n---\n"; + let blocks: Vec<&str> = block_ranges(text) + .into_iter() + .map(|range| &text[range]) + .collect(); + assert_eq!(blocks, ["# Lift", "Blend **it**.", "- a\n- b", "---"]); + } + + #[test] + fn reading_takes_a_minute_per_pace() { + assert_eq!(words("Blend toward white, by lift."), 5); + assert_eq!(words("你好世界"), 4); + assert_eq!( + (reading_minutes(0), reading_minutes(1), reading_minutes(239)), + (0, 1, 2) + ); + } +} diff --git a/src/documents/mod.rs b/src/documents/mod.rs new file mode 100644 index 0000000..a3fb3b4 --- /dev/null +++ b/src/documents/mod.rs @@ -0,0 +1,32 @@ +mod blocks; +mod counts; +mod format; +mod link; +mod markdown; +mod modes; +mod render; +mod rich; +mod suggest; +#[cfg(all(test, feature = "test-support"))] +mod tests; +mod todo; +mod toolbar; +mod tree; + +use gpui::App; + +pub use blocks::{Align, BlockData, BlockEditor, BlockEvent, BlockKind, Media}; +pub use counts::{ReadingTime, WordCount}; +pub use format::{Format, format}; +pub use link::LinkEditor; +pub use markdown::markdown_highlights; +pub use modes::{MarkdownEditor, MarkdownMode}; +pub use render::MarkdownRenderer; +pub use rich::RichTextEditor; +pub use todo::{Checklist, TodoItem}; +pub use toolbar::FixedFormatToolbar; + +/// Binds the documents keys. `init` calls it. +pub(crate) fn bind_keys(cx: &mut App) { + rich::bind_keys(cx); +} diff --git a/src/documents/modes.rs b/src/documents/modes.rs new file mode 100644 index 0000000..47f1b22 --- /dev/null +++ b/src/documents/modes.rs @@ -0,0 +1,467 @@ +use std::{ops::Range, rc::Rc}; + +use gpui::{ + AnyElement, App, AppContext as _, ElementId, Entity, Focusable, InteractiveElement, + IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, Styled, Subscription, + Window, div, prelude::*, +}; + +use super::{ + counts::{ReadingTime, WordCount}, + markdown::{block_ranges, markdown_highlights}, + render::MarkdownRenderer, + rich::RichTextEditor, + suggest::offers, + toolbar::FixedFormatToolbar, +}; +use crate::{ + buttons::SegmentedControl, + forms::{Down, InputEvent, TextInput, Up}, + theme::{ActiveTheme, ControlSize, Radius, TextSize}, +}; + +/// How a markdown editor shows its text. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MarkdownMode { + /// The document as it reads; a pressed block opens as its source. + Visual, + /// The markdown, styled as it is typed. + Source, + /// The source beside the document it makes. + Split, +} + +impl MarkdownMode { + const ALL: [(Self, &'static str); 3] = [ + (Self::Visual, "Visual"), + (Self::Source, "Source"), + (Self::Split, "Split"), + ]; + + fn name(self) -> &'static str { + Self::ALL + .iter() + .find(|(mode, _)| *mode == self) + .map(|(_, name)| *name) + .expect("every mode is named") + } +} + +type OnMode = Rc; + +/// The block open as its source: where it came from, its own field, and whether it adds a block at the end. +struct Open { + range: Range, + input: Entity, + append: bool, + _blur: Subscription, +} + +#[derive(Default)] +struct Visual { + open: Option, +} + +/// `range` moved by an edit that wrote `written` bytes over `replaced`, when it lies after it. +fn shifted(range: Range, replaced: &Range, written: usize) -> Range { + if range.start < replaced.end { + return range; + } + let (start, end) = ( + range.start - replaced.len() + written, + range.end - replaced.len() + written, + ); + start..end +} + +/// What joins a block added at the end to the text before it. +fn joint(document: &str) -> &'static str { + if document.is_empty() || document.ends_with("\n\n") { + "" + } else if document.ends_with('\n') { + "\n" + } else { + "\n\n" + } +} + +/// Writes the open block back over its range as one undo step, and closes it; the range replaced and the bytes written. +fn commit( + state: &Entity, + field: &Entity, + cx: &mut App, +) -> Option<(Range, usize)> { + let open = state.update(cx, |visual, cx| { + cx.notify(); + visual.open.take() + })?; + let written = open.input.read(cx).text().to_string(); + let document = field.read(cx).text().to_string(); + let written = match open.append { + true if written.is_empty() => return Some((open.range, 0)), + true => format!("{}{written}", joint(&document)), + false => written, + }; + if document[open.range.clone()] != written { + log::info!("markdown editor: block {:?} written back", open.range); + field.update(cx, |document, cx| { + document.select(open.range.clone(), cx); + document.insert(&written, cx); + }); + } + Some((open.range, written.len())) +} + +/// Opens the block at `range` in a field of its own, so undo stays with it; the caret at its start or end. +fn open( + state: &Entity, + field: &Entity, + range: Range, + append: bool, + at_start: bool, + window: &mut Window, + cx: &mut App, +) { + let source = field.read(cx).text()[range.clone()].to_string(); + log::info!("markdown editor: block {range:?} opens"); + let input = cx.new(|cx| { + TextInput::new(window, cx) + .multi_line(1, usize::MAX) + .highlighter(markdown_highlights) + }); + input.update(cx, |input, cx| { + input.set_text(source, cx); + if at_start { + input.select(0..0, cx); + } + }); + window.focus(&input.focus_handle(cx)); + let blur = { + let (state, field) = (state.clone(), field.clone()); + cx.subscribe(&input, move |_, event: &InputEvent, cx| { + if *event == InputEvent::Blur { + commit(&state, &field, cx); + } + }) + }; + state.update(cx, |visual, cx| { + visual.open = Some(Open { + range, + input, + append, + _blur: blur, + }); + cx.notify(); + }); +} + +/// Whether the caret sits on the field's first line, or with `last` its last. +pub(crate) fn at_edge(input: &TextInput, last: bool) -> bool { + let edge = if last { input.text().len() } else { 0 }; + match (input.bounds_for(input.cursor()), input.bounds_for(edge)) { + (Some(caret), Some(edge)) => caret.top() == edge.top(), + _ => false, + } +} + +/// Markdown written three ways: visual, where the document reads as it will and a pressed block opens as its source; source, styled as it is typed; and split, the source beside the document. The toolbar, links, suggestions and counts come with each. +#[derive(IntoElement)] +pub struct MarkdownEditor { + id: ElementId, + field: Entity, + mode: MarkdownMode, + people: Vec, + on_mode: Option, +} + +impl MarkdownEditor { + /// Give the field several lines and `markdown_highlights` as its highlighter. + pub fn new(id: impl Into, field: &Entity, mode: MarkdownMode) -> Self { + Self { + id: id.into(), + field: field.clone(), + mode, + people: Vec::new(), + on_mode: None, + } + } + + /// Handles `@` suggests. + pub fn people(mut self, handles: impl IntoIterator>) -> Self { + self.people = handles.into_iter().map(Into::into).collect(); + self + } + + pub fn on_mode( + mut self, + handler: impl Fn(MarkdownMode, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_mode = Some(Rc::new(handler)); + self + } +} + +impl RenderOnce for MarkdownEditor { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let state = + window.use_keyed_state((self.id.clone(), "visual"), cx, |_, _| Visual::default()); + let switch = { + let (state, field, on_mode) = (state.clone(), self.field.clone(), self.on_mode.clone()); + MarkdownMode::ALL + .iter() + .fold( + SegmentedControl::new((self.id.clone(), "mode"), self.mode.name()) + .size(ControlSize::Sm), + |switch, (_, name)| switch.segment(*name, *name, None), + ) + .on_change(move |name, window, cx| { + let (mode, _) = MarkdownMode::ALL + .into_iter() + .find(|(_, named)| *named == name.as_ref()) + .expect("a segment names a mode"); + commit(&state, &field, cx); + log::info!("markdown editor: {name}"); + if let Some(on_mode) = &on_mode { + on_mode(mode, window, cx); + } + }) + }; + match self.mode { + MarkdownMode::Source => RichTextEditor::new(self.id, &self.field) + .people(self.people) + .toolbar_end(switch) + .into_any_element(), + MarkdownMode::Split => { + let text = SharedString::from(self.field.read(cx).text().to_string()); + RichTextEditor::new(self.id.clone(), &self.field) + .people(self.people) + .toolbar_end(switch) + .beside(MarkdownRenderer::new((self.id, "preview"), text)) + .into_any_element() + } + MarkdownMode::Visual => visual(self, state, switch, window, cx), + } + } +} + +/// The visual body: each block rendered, the open one as its source. +fn visual( + editor: MarkdownEditor, + state: Entity, + switch: SegmentedControl, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let (colors, radius, frame, bar, tiny) = { + let theme = cx.theme(); + ( + theme.colors.clone(), + theme.radius(Radius::Md), + theme.radius(Radius::Lg), + theme.control_height(ControlSize::Lg), + theme.text_size(TextSize::Xs), + ) + }; + let document = editor.field.read(cx).text().to_string(); + let blocks = block_ranges(&document); + let opened = state + .read(cx) + .open + .as_ref() + .map(|open| (open.range.clone(), open.input.clone(), open.append)); + let field = editor.field.clone(); + let source = |input: &Entity, window: &mut Window, cx: &mut App| { + div() + .px_3() + .py_2() + .rounded(radius) + .bg(colors.sunken) + .child( + offers( + (editor.id.clone(), "offers").into(), + input, + &editor.people, + None, + cx, + ) + .wrap(input.clone(), window, cx), + ) + .into_any_element() + }; + let mut rows: Vec = Vec::new(); + for (ix, range) in blocks.iter().enumerate() { + if let Some((open_range, input, false)) = &opened + && open_range == range + { + rows.push(source(input, window, cx)); + continue; + } + let (state, field, pressed) = (state.clone(), field.clone(), range.clone()); + rows.push( + div() + .id((editor.id.clone(), format!("block-{ix}"))) + .px_3() + .py_1() + .rounded(radius) + .cursor_text() + .hover(|block| block.bg(colors.hover.opacity(0.5))) + .on_mouse_down(MouseButton::Left, move |_, window, cx| { + window.prevent_default(); + cx.stop_propagation(); + let range = match commit(&state, &field, cx) { + Some((replaced, written)) => shifted(pressed.clone(), &replaced, written), + None => pressed.clone(), + }; + open(&state, &field, range, false, false, window, cx); + }) + .child(MarkdownRenderer::new( + (editor.id.clone(), format!("render-{ix}")), + document[range.clone()].to_string(), + )) + .into_any_element(), + ); + } + if let Some((_, input, true)) = &opened { + rows.push(source(input, window, cx)); + } + let (more_state, more_field) = (state.clone(), field.clone()); + let more = div() + .id((editor.id.clone(), "more")) + .px_3() + .py_2() + .cursor_text() + .text_color(colors.fg_subtle) + .on_mouse_down(MouseButton::Left, move |_, window, cx| { + window.prevent_default(); + commit(&more_state, &more_field, cx); + let end = more_field.read(cx).text().len(); + open(&more_state, &more_field, end..end, true, false, window, cx); + }) + .when(document.is_empty() && opened.is_none(), |more| { + more.child("Start writing") + }) + .when(!document.is_empty(), |more| more.min_h_8()); + let open_input = opened.as_ref().map(|(_, input, _)| input.clone()); + let edges = |last: bool| { + let (state, field) = (state.clone(), field.clone()); + move |window: &mut Window, cx: &mut App| -> bool { + let Some((range, input)) = state + .read(cx) + .open + .as_ref() + .map(|open| (open.range.clone(), open.input.clone())) + else { + return false; + }; + if !at_edge(input.read(cx), last) { + return false; + } + let blocks = block_ranges(field.read(cx).text()); + let target = if last { + blocks.into_iter().find(|block| block.start >= range.end) + } else { + blocks + .into_iter() + .rev() + .find(|block| block.end <= range.start) + }; + let Some(target) = target else { return false }; + let target = match commit(&state, &field, cx) { + Some((replaced, written)) => shifted(target, &replaced, written), + None => target, + }; + open(&state, &field, target, false, last, window, cx); + true + } + }; + let (up, down) = (edges(false), edges(true)); + let (escape_state, escape_field) = (state.clone(), field.clone()); + let text = SharedString::from(document); + div() + .id(editor.id.clone()) + .flex() + .flex_col() + .rounded(frame) + .border_1() + .border_color(colors.border) + .bg(colors.surface) + .child( + div() + .flex() + .items_center() + .justify_between() + .h(bar) + .px_2() + .border_b_1() + .border_color(colors.border) + .child( + div().children(open_input.map(|input| { + FixedFormatToolbar::new((editor.id.clone(), "tools"), &input) + })), + ) + .child(switch), + ) + .child( + div() + .id((editor.id.clone(), "blocks")) + .flex() + .flex_col() + .gap_1() + .px_2() + .py_3() + .capture_action(move |_: &Up, window, cx| { + if up(window, cx) { + cx.stop_propagation(); + } + }) + .capture_action(move |_: &Down, window, cx| { + if down(window, cx) { + cx.stop_propagation(); + } + }) + .on_key_down(move |event, _, cx| { + if event.keystroke.key == "escape" && escape_state.read(cx).open.is_some() { + cx.stop_propagation(); + commit(&escape_state, &escape_field, cx); + } + }) + .children(rows) + .child(more), + ) + .child( + div() + .flex() + .justify_between() + .px_4() + .py_2() + .border_t_1() + .border_color(colors.border) + .text_size(tiny) + .child(WordCount::new(text.clone())) + .child(ReadingTime::new(text)), + ) + .into_any_element() +} + +#[cfg(test)] +mod tests { + use super::{joint, shifted}; + + #[test] + fn later_blocks_move_by_what_an_edit_wrote() { + assert_eq!(shifted(10..14, &(2..6), 9), 15..19); + assert_eq!( + shifted(0..2, &(2..6), 9), + 0..2, + "a block before the edit stays" + ); + } + + #[test] + fn a_block_at_the_end_starts_a_paragraph() { + assert_eq!( + (joint(""), joint("a"), joint("a\n"), joint("a\n\n")), + ("", "\n\n", "\n", "") + ); + } +} diff --git a/src/documents/render.rs b/src/documents/render.rs new file mode 100644 index 0000000..3f26f01 --- /dev/null +++ b/src/documents/render.rs @@ -0,0 +1,357 @@ +use std::rc::Rc; + +use gpui::{ + AnyElement, App, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, + IntoElement, ParentElement, RenderOnce, SharedString, StrikethroughStyle, Styled, StyledText, + UnderlineStyle, Window, div, prelude::*, +}; +use pulldown_cmark::Alignment; + +use super::tree::{Inline, Node, Style, parse}; +use crate::{ + editor::code_colors, + primitives::{Icon, IconName}, + theme::{ActiveTheme, IconSize, Radius, TextSize}, + typography::Latex, +}; + +type OnLink = Rc; + +/// Markdown as a document: headings, prose with its styles and links, lists and tasks, quotes, code colored as code, tables, rules, math and footnotes. A press on a link opens it, or hands it to the host. +#[derive(IntoElement)] +pub struct MarkdownRenderer { + id: ElementId, + source: SharedString, + on_link: Option, +} + +impl MarkdownRenderer { + pub fn new(id: impl Into, source: impl Into) -> Self { + Self { + id: id.into(), + source: source.into(), + on_link: None, + } + } + + /// Takes link presses instead of the browser. + pub fn on_link(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self { + self.on_link = Some(Rc::new(handler)); + self + } +} + +/// Renders nodes with one set of links handlers and ids under one owner. +struct Draw<'a> { + id: &'a ElementId, + on_link: &'a Option, + next: std::cell::Cell, +} + +impl Draw<'_> { + fn key(&self, what: &str) -> ElementId { + let at = self.next.get(); + self.next.set(at + 1); + (self.id.clone(), format!("{what}-{at}")).into() + } + + fn inline(&self, inline: &Inline, cx: &App) -> AnyElement { + let theme = cx.theme(); + let colors = &theme.colors; + let rule = theme.underline_thickness(); + let styles: Vec<_> = inline + .styles + .iter() + .map(|(range, style)| { + let highlight = match style { + Style::Strong => HighlightStyle { + font_weight: Some(FontWeight::SEMIBOLD), + ..HighlightStyle::default() + }, + Style::Emphasis => HighlightStyle { + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + Style::Strike => HighlightStyle { + strikethrough: Some(StrikethroughStyle { + thickness: rule, + color: Some(colors.fg_muted), + }), + ..HighlightStyle::default() + }, + Style::Code | Style::Math => HighlightStyle { + background_color: Some(colors.sunken), + color: Some(if *style == Style::Math { + colors.syntax.function + } else { + colors.fg + }), + ..HighlightStyle::default() + }, + Style::Link => HighlightStyle { + color: Some(colors.link), + underline: Some(UnderlineStyle { + thickness: rule, + color: Some(colors.link.opacity(0.4)), + wavy: false, + }), + ..HighlightStyle::default() + }, + Style::Footnote => HighlightStyle { + color: Some(colors.fg_subtle), + ..HighlightStyle::default() + }, + }; + (range.clone(), highlight) + }) + .collect(); + let styled = + StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles)); + if inline.links.is_empty() { + return styled.into_any_element(); + } + let (ranges, urls): (Vec<_>, Vec<_>) = inline.links.iter().cloned().unzip(); + let on_link = self.on_link.clone(); + InteractiveText::new(self.key("links"), styled) + .on_click(ranges, move |ix, window, cx| { + let url = &urls[ix]; + log::info!("markdown: open {url}"); + match &on_link { + Some(on_link) => on_link(url, window, cx), + None => cx.open_url(url), + } + }) + .into_any_element() + } + + fn nodes(&self, nodes: &[Node], cx: &App) -> Vec { + nodes.iter().map(|node| self.node(node, cx)).collect() + } + + fn node(&self, node: &Node, cx: &App) -> AnyElement { + let theme = cx.theme(); + let colors = theme.colors.clone(); + match node { + Node::Paragraph(inline) => div() + .text_color(colors.fg) + .child(self.inline(inline, cx)) + .into_any_element(), + Node::Heading(level, inline) => { + let size = match level { + 1 => TextSize::Xxl, + 2 => TextSize::Xl, + 3 => TextSize::Lg, + _ => TextSize::Md, + }; + div() + .pt_2() + .text_size(theme.text_size(size)) + .font_weight(if *level <= 2 { + FontWeight::SEMIBOLD + } else { + FontWeight::MEDIUM + }) + .text_color(colors.fg) + .child(self.inline(inline, cx)) + .into_any_element() + } + Node::Quote(inner) => div() + .flex() + .flex_col() + .gap_2() + .pl_4() + .border_l_2() + .border_color(colors.border_strong) + .text_color(colors.fg_muted) + .children(self.nodes(inner, cx)) + .into_any_element(), + Node::Code { language, text } => { + let styles = code_colors(text, cx); + div() + .flex() + .flex_col() + .gap_1() + .p_3() + .rounded(theme.radius(Radius::Md)) + .bg(colors.sunken) + .font_family(theme.mono_family.clone()) + .text_size(theme.text_size(TextSize::Sm)) + .text_color(colors.syntax.variable) + .children(language.clone().map(|name| { + div() + .text_size(theme.text_size(TextSize::Xs)) + .text_color(colors.fg_subtle) + .child(name) + })) + .child(StyledText::new(text.clone()).with_highlights(styles)) + .into_any_element() + } + Node::List { start, items } => div() + .flex() + .flex_col() + .gap_1() + .children(items.iter().enumerate().map(|(ix, (task, body))| { + let marker: AnyElement = match (task, start) { + (Some(done), _) => Icon::new(if *done { + IconName::SquareCheck + } else { + IconName::Square + }) + .size(IconSize::Sm) + .color(if *done { + colors.focus + } else { + colors.fg_subtle + }) + .into_any_element(), + (None, Some(first)) => div() + .text_color(colors.fg_muted) + .child(format!("{}.", first + ix as u64)) + .into_any_element(), + (None, None) => div() + .text_color(colors.fg_muted) + .child("•") + .into_any_element(), + }; + div() + .flex() + .gap_2() + .child( + div() + .flex_none() + .min_w_5() + .flex() + .justify_end() + .child(marker), + ) + .child( + div() + .flex_1() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .when(*task == Some(true), |body| { + body.text_color(colors.fg_subtle) + }) + .children(self.nodes(body, cx)), + ) + })) + .into_any_element(), + Node::Table { aligns, head, rows } => { + let cell = |inline: &Inline, column: usize, header: bool, cx: &App| { + div() + .flex_1() + .flex() + .px_3() + .py_1p5() + .when(aligns.get(column) == Some(&Alignment::Right), |cell| { + cell.justify_end() + }) + .when(aligns.get(column) == Some(&Alignment::Center), |cell| { + cell.justify_center() + }) + .when(header, |cell| { + cell.font_weight(FontWeight::MEDIUM) + .text_color(colors.fg_muted) + }) + .child(self.inline(inline, cx)) + }; + div() + .flex() + .flex_col() + .rounded(theme.radius(Radius::Md)) + .border_1() + .border_color(colors.border) + .overflow_hidden() + .child( + div().flex().bg(colors.hover).children( + head.iter() + .enumerate() + .map(|(column, inline)| cell(inline, column, true, cx)), + ), + ) + .children(rows.iter().map(|row| { + div() + .flex() + .border_t_1() + .border_color(colors.border) + .children( + row.iter() + .enumerate() + .map(|(column, inline)| cell(inline, column, false, cx)), + ) + })) + .into_any_element() + } + Node::Rule => div() + .my_2() + .h_0() + .border_t_1() + .border_color(colors.border) + .into_any_element(), + Node::Math(tex) => div() + .flex() + .justify_center() + .py_2() + .child(match Latex::parse(tex) { + Ok(math) => math.into_any_element(), + Err(error) => div() + .text_color(colors.danger) + .child(error.to_string()) + .into_any_element(), + }) + .into_any_element(), + Node::Image { alt, url } => div() + .flex() + .items_center() + .gap_2() + .p_3() + .rounded(theme.radius(Radius::Md)) + .border_1() + .border_color(colors.border) + .text_color(colors.fg_muted) + .child( + Icon::new(IconName::Image) + .size(IconSize::Sm) + .color(colors.fg_subtle), + ) + .child(div().flex_1().child(alt.clone())) + .child(div().text_color(colors.fg_subtle).child(url.clone())) + .into_any_element(), + Node::Footnote { label, body } => div() + .flex() + .gap_2() + .text_size(theme.text_size(TextSize::Sm)) + .text_color(colors.fg_muted) + .child(div().flex_none().child(format!("[{label}]"))) + .child( + div() + .flex_1() + .flex() + .flex_col() + .children(self.nodes(body, cx)), + ) + .into_any_element(), + } + } +} + +impl RenderOnce for MarkdownRenderer { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let theme = cx.theme(); + let nodes = parse(&self.source); + let draw = Draw { + id: &self.id, + on_link: &self.on_link, + next: std::cell::Cell::new(0), + }; + div() + .flex() + .flex_col() + .gap_3() + .text_size(theme.text_size(TextSize::Base)) + .line_height(theme.text_size(TextSize::Base) * 1.6) + .children(draw.nodes(&nodes, cx)) + } +} diff --git a/src/documents/rich.rs b/src/documents/rich.rs new file mode 100644 index 0000000..dce1e25 --- /dev/null +++ b/src/documents/rich.rs @@ -0,0 +1,186 @@ +use std::rc::Rc; + +use gpui::{ + AnyElement, App, ElementId, Entity, InteractiveElement, IntoElement, KeyBinding, ParentElement, + RenderOnce, SharedString, Styled, Window, actions, div, prelude::*, +}; + +use super::{ + counts::{ReadingTime, WordCount}, + format::{Format, active, format}, + link::LinkEditor, + suggest::offers, + toolbar::{FixedFormatToolbar, INLINE, tool}, +}; +use crate::{ + forms::{Run, TextInput}, + overlays::FloatingToolbar, + theme::{ActiveTheme, Radius, TextSize}, +}; + +actions!(ely_rich_text, [ToggleBold, ToggleItalic, EditLink]); + +const CONTEXT: &str = "ElyRichText"; + +/// Binds the rich text keys. `init` calls it. +pub(crate) fn bind_keys(cx: &mut App) { + cx.bind_keys([ + KeyBinding::new("secondary-b", ToggleBold, Some(CONTEXT)), + KeyBinding::new("secondary-i", ToggleItalic, Some(CONTEXT)), + KeyBinding::new("secondary-k", EditLink, Some(CONTEXT)), + ]); +} + +/// Markdown that reads as it is written: styles show as they are typed, the toolbar above and the one over a selection apply them, a link opens its editor, `@` and `:` suggest people and emoji, and the foot counts words and minutes. +#[derive(IntoElement)] +pub struct RichTextEditor { + id: ElementId, + field: Entity, + people: Vec, + end: Option, + beside: Option, +} + +impl RichTextEditor { + /// Give the field several lines and `markdown_highlights` as its highlighter. + pub fn new(id: impl Into, field: &Entity) -> Self { + Self { + id: id.into(), + field: field.clone(), + people: Vec::new(), + end: None, + beside: None, + } + } + + /// Sits at the toolbar's far end, such as a switch of views. + pub fn toolbar_end(mut self, element: impl IntoElement) -> Self { + self.end = Some(element.into_any_element()); + self + } + + /// Sits beside the text, as a preview does. + pub fn beside(mut self, element: impl IntoElement) -> Self { + self.beside = Some(element.into_any_element()); + self + } + + /// Handles `@` suggests. + pub fn people(mut self, handles: impl IntoIterator>) -> Self { + self.people = handles.into_iter().map(Into::into).collect(); + self + } +} + +impl RenderOnce for RichTextEditor { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let input = self.field.read(cx); + assert!( + input.is_multi_line(), + "rich text {:?} needs a field of several lines", + self.id + ); + let text = SharedString::from(input.text().to_string()); + let pressed = active(input.text(), input.selection()); + let linking = window.use_keyed_state((self.id.clone(), "linking"), cx, |_, _| false); + let set_linking = |open: bool| -> Run { + let linking = linking.clone(); + Rc::new(move |_, cx| { + log::info!( + "rich text: link editor {}", + if open { "opens" } else { "closes" } + ); + linking.update(cx, |linking, cx| { + *linking = open; + cx.notify(); + }); + }) + }; + let (open_link, close_link) = (set_linking(true), set_linking(false)); + let float_id: ElementId = (self.id.clone(), "float").into(); + let floating = FloatingToolbar::new(float_id.clone(), &self.field).children( + INLINE.iter().map(|tool_of| { + let on = pressed.contains(&tool_of.0); + tool( + &float_id, + &self.field, + *tool_of, + on, + Some(open_link.clone()), + ) + }), + ); + let suggestions = offers( + (self.id.clone(), "offers").into(), + &self.field, + &self.people, + None, + cx, + ) + .wrap(self.field.clone(), window, cx); + let theme = cx.theme(); + let colors = theme.colors.clone(); + let (bold, italic, link) = (self.field.clone(), self.field.clone(), open_link.clone()); + div() + .id(self.id.clone()) + .key_context(CONTEXT) + .on_action(move |_: &ToggleBold, _, cx| format(&bold, Format::Bold, cx)) + .on_action(move |_: &ToggleItalic, _, cx| format(&italic, Format::Italic, cx)) + .on_action(move |_: &EditLink, window, cx| link(window, cx)) + .flex() + .flex_col() + .rounded(theme.radius(Radius::Lg)) + .border_1() + .border_color(colors.border) + .bg(colors.surface) + .child( + div() + .flex() + .items_center() + .justify_between() + .px_2() + .border_b_1() + .border_color(colors.border) + .child( + FixedFormatToolbar::new((self.id.clone(), "tools"), &self.field) + .on_link(move |window, cx| open_link(window, cx)), + ) + .children(self.end), + ) + .child( + div() + .flex() + .child(div().flex_1().min_w_0().px_4().py_3().child(suggestions)) + .children(self.beside.map(|beside| { + div() + .flex_1() + .min_w_0() + .px_4() + .py_3() + .border_l_1() + .border_color(colors.border) + .child(beside) + })), + ) + .child(floating) + .child( + div() + .flex() + .justify_between() + .px_4() + .py_2() + .border_t_1() + .border_color(colors.border) + .text_size(theme.text_size(TextSize::Xs)) + .child(WordCount::new(text.clone())) + .child(ReadingTime::new(text)), + ) + .when(*linking.read(cx), |editor| { + editor.child(LinkEditor::new( + (self.id.clone(), "link"), + &self.field, + move |window, cx| close_link(window, cx), + )) + }) + } +} diff --git a/src/documents/suggest.rs b/src/documents/suggest.rs new file mode 100644 index 0000000..c40eb92 --- /dev/null +++ b/src/documents/suggest.rs @@ -0,0 +1,104 @@ +use std::rc::Rc; + +use gpui::{App, ElementId, Entity, SharedString, Window}; + +use crate::forms::{ + Choice, Pick, Suggestions, TextInput, active_trigger, emoji_found, handles_matching, +}; + +/// A trigger of the owner's: from the query and where it runs, the rows and what a pick does. +pub(crate) type Offer<'a> = &'a dyn Fn(&str, usize, usize) -> (Vec, Pick); + +/// An emoji name needs this many letters before `:` suggests, so a colon in prose stays quiet. +const EMOJI_QUERY: usize = 2; + +/// Emoji shown for a `:` query. +const EMOJI_SHOWN: usize = 6; + +/// An offer of nothing: no rows, so no pick can come. +fn nothing() -> (Vec, Pick) { + ( + Vec::new(), + Rc::new(|_: usize, _: &mut Window, _: &mut App| unreachable!("an empty offer has no row")), + ) +} + +/// Replaces the trigger and its query, `at..caret`, with `text`, as one undo step. +pub(crate) fn replace_trigger( + field: &Entity, + at: usize, + caret: usize, + text: &str, + cx: &mut App, +) { + field.update(cx, |input, cx| { + input.select(at..caret, cx); + input.insert(text, cx); + }); +} + +/// What `@` and `:` offer at the field's caret: people by handle, emoji by name; `extra` adds a trigger of the owner's, such as `/` in blocks. +pub(crate) fn offers( + id: ElementId, + field: &Entity, + people: &[SharedString], + extra: Option<(char, Offer)>, + cx: &App, +) -> Suggestions { + let input = field.read(cx); + let (text, caret) = (input.text().to_string(), input.cursor()); + let mut marks = vec!['@', ':']; + marks.extend(extra.as_ref().map(|(mark, _)| *mark)); + let active = active_trigger(&text, caret, &marks); + let query = active.map_or("", |(at, mark)| &text[at + mark.len_utf8()..caret]); + let (rows, pick): (Vec, Pick) = match active { + Some((at, '@')) => { + let found = handles_matching(people, query); + let rows = found + .iter() + .map(|name| Choice::new(name.clone(), format!("@{name}"))) + .collect(); + let field = field.clone(); + ( + rows, + Rc::new(move |pick: usize, _: &mut Window, cx: &mut App| { + log::info!("mention: @{}", found[pick]); + replace_trigger(&field, at, caret, &format!("@{} ", found[pick]), cx); + }), + ) + } + Some((at, ':')) if query.len() >= EMOJI_QUERY => { + let found: Vec<&'static emojis::Emoji> = emoji_found(&query.to_lowercase()) + .into_iter() + .take(EMOJI_SHOWN) + .collect(); + let rows = found + .iter() + .map(|emoji| { + let name = emoji.shortcode().unwrap_or(emoji.name()); + Choice::new(name, format!("{} {name}", emoji.as_str())) + }) + .collect(); + let field = field.clone(); + ( + rows, + Rc::new(move |pick: usize, _: &mut Window, cx: &mut App| { + log::info!("emoji: {}", found[pick].name()); + replace_trigger(&field, at, caret, found[pick].as_str(), cx); + }), + ) + } + Some((at, mark)) => match &extra { + Some((extra_mark, build)) if *extra_mark == mark => build(query, at, caret), + _ => nothing(), + }, + None => nothing(), + }; + Suggestions { + id, + state: field.clone(), + trigger: active.map(|(at, _)| at), + rows, + pick, + } +} diff --git a/src/documents/tests.rs b/src/documents/tests.rs new file mode 100644 index 0000000..739b313 --- /dev/null +++ b/src/documents/tests.rs @@ -0,0 +1,184 @@ +use gpui::{AppContext as _, Entity, Focusable, TestAppContext, VisualTestContext}; + +use super::{BlockData, BlockEditor, BlockKind}; +use crate::{forms, theme::Theme}; + +fn open( + blocks: Vec, + cx: &mut TestAppContext, +) -> (Entity, &mut VisualTestContext) { + cx.update(|cx| { + Theme::init(cx); + forms::bind_keys(cx); + super::bind_keys(cx); + }); + let (editor, cx) = cx.add_window_view(|window, cx| BlockEditor::new(blocks, window, cx)); + settle(cx); + (editor, cx) +} + +fn settle(cx: &mut VisualTestContext) { + cx.run_until_parked(); + cx.update(|window, _| window.refresh()); + cx.run_until_parked(); +} + +fn blocks( + editor: &Entity, + cx: &mut VisualTestContext, +) -> Vec<(BlockKind, Vec)> { + cx.update(|_, cx| { + editor + .read(cx) + .blocks(cx) + .into_iter() + .map(|block| (block.kind, block.texts)) + .collect() + }) +} + +/// Puts the caret in block `ix`'s first field at byte `at`. +fn caret(editor: &Entity, ix: usize, at: usize, cx: &mut VisualTestContext) { + cx.update(|window, cx| { + let field = editor.read(cx).blocks[ix].fields[0].clone(); + field.update(cx, |field, cx| field.select(at..at, cx)); + window.focus(&field.focus_handle(cx)); + }); + settle(cx); +} + +fn text(kind: BlockKind, text: &str) -> (BlockKind, Vec) { + (kind, vec![text.to_string()]) +} + +#[gpui::test] +fn enter_splits_prose_at_the_caret(cx: &mut TestAppContext) { + let (editor, cx) = open(vec![BlockData::new(BlockKind::Bullet, ["lift"])], cx); + caret(&editor, 0, 2, cx); + cx.simulate_keystrokes("enter"); + settle(cx); + assert_eq!( + blocks(&editor, cx), + [text(BlockKind::Bullet, "li"), text(BlockKind::Bullet, "ft")] + ); +} + +#[gpui::test] +fn backspace_at_the_start_joins_up_and_undo_parts_them(cx: &mut TestAppContext) { + let (editor, cx) = open( + vec![ + BlockData::new(BlockKind::Paragraph, ["tone "]), + BlockData::new(BlockKind::Paragraph, ["lift"]), + ], + cx, + ); + caret(&editor, 1, 0, cx); + cx.simulate_keystrokes("backspace"); + settle(cx); + assert_eq!( + blocks(&editor, cx), + [text(BlockKind::Paragraph, "tone lift")] + ); + cx.simulate_keystrokes("cmd-z"); + settle(cx); + assert_eq!( + blocks(&editor, cx), + [ + text(BlockKind::Paragraph, "tone "), + text(BlockKind::Paragraph, "lift") + ], + "undo brings the joined block back" + ); +} + +#[gpui::test] +fn markdown_typed_at_a_paragraphs_start_changes_its_kind(cx: &mut TestAppContext) { + let (editor, cx) = open(vec![BlockData::new(BlockKind::Paragraph, [""])], cx); + caret(&editor, 0, 0, cx); + cx.simulate_input("## Lift"); + settle(cx); + assert_eq!(blocks(&editor, cx), [text(BlockKind::Heading(2), "Lift")]); +} + +#[gpui::test] +fn backspace_turns_a_heading_back_into_text(cx: &mut TestAppContext) { + let (editor, cx) = open(vec![BlockData::new(BlockKind::Heading(1), ["Lift"])], cx); + caret(&editor, 0, 0, cx); + cx.simulate_keystrokes("backspace"); + settle(cx); + assert_eq!(blocks(&editor, cx), [text(BlockKind::Paragraph, "Lift")]); +} + +#[gpui::test] +fn a_press_in_a_blocks_text_puts_the_caret_there(cx: &mut TestAppContext) { + let (editor, cx) = open(vec![BlockData::new(BlockKind::Paragraph, ["lift"])], cx); + cx.simulate_click( + gpui::point(gpui::px(300.0), gpui::px(10.0)), + gpui::Modifiers::none(), + ); + settle(cx); + let focused = cx.update(|window, cx| editor.read(cx).focused(window, cx)); + assert!(focused.is_some(), "the press focuses the block's field"); + cx.simulate_keystrokes("enter"); + settle(cx); + assert_eq!(blocks(&editor, cx).len(), 2); +} + +/// The editor at a fixed width inside a row, as a page holds it. +struct Hosted(Entity); + +impl gpui::Render for Hosted { + fn render( + &mut self, + _: &mut gpui::Window, + _: &mut gpui::Context, + ) -> impl gpui::IntoElement { + use gpui::{ParentElement, Styled}; + gpui::div() + .flex() + .child(gpui::div().w(gpui::px(760.0)).child(self.0.clone())) + } +} + +#[gpui::test] +fn a_press_past_the_words_still_lands_in_the_block(cx: &mut TestAppContext) { + cx.update(|cx| { + Theme::init(cx); + forms::bind_keys(cx); + super::bind_keys(cx); + }); + let (host, cx) = cx.add_window_view(|window, cx| { + let editor = cx.new(|cx| { + BlockEditor::new( + vec![BlockData::new(BlockKind::Paragraph, ["lift"])], + window, + cx, + ) + }); + Hosted(editor) + }); + settle(cx); + cx.simulate_click( + gpui::point(gpui::px(700.0), gpui::px(10.0)), + gpui::Modifiers::none(), + ); + settle(cx); + let editor = cx.update(|_, cx| host.read(cx).0.clone()); + let focused = cx.update(|window, cx| editor.read(cx).focused(window, cx)); + assert!(focused.is_some(), "the press focuses the block's field"); +} + +#[gpui::test] +fn a_press_beside_a_blocks_text_puts_the_caret_at_its_end(cx: &mut TestAppContext) { + let (editor, cx) = open(vec![BlockData::new(BlockKind::Paragraph, ["lift"])], cx); + cx.simulate_click( + gpui::point(gpui::px(300.0), gpui::px(1.0)), + gpui::Modifiers::none(), + ); + settle(cx); + let focused = cx.update(|window, cx| editor.read(cx).focused(window, cx)); + assert_eq!( + focused, + Some((editor.read_with(cx, |editor, _| editor.blocks[0].key), 0)) + ); +} diff --git a/src/documents/todo.rs b/src/documents/todo.rs new file mode 100644 index 0000000..c948126 --- /dev/null +++ b/src/documents/todo.rs @@ -0,0 +1,146 @@ +use std::rc::Rc; + +use gpui::{ + App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled, + Window, div, prelude::*, +}; + +use crate::{ + forms::{Checkbox, OnFlag}, + motion::ProgressBar, + theme::{ActiveTheme, TextSize}, + typography::tabular, +}; + +type OnItem = Rc; + +/// A to-do: its box and its words; done, the words go quiet and struck through. +#[derive(IntoElement)] +pub struct TodoItem { + id: ElementId, + label: SharedString, + done: bool, + on_toggle: Option, +} + +impl TodoItem { + pub fn new(id: impl Into, label: impl Into, done: bool) -> Self { + Self { + id: id.into(), + label: label.into(), + done, + on_toggle: None, + } + } + + pub fn on_toggle(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self { + self.on_toggle = Some(Rc::new(handler)); + self + } +} + +impl RenderOnce for TodoItem { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let colors = &cx.theme().colors; + let check = Checkbox::new(self.id, self.done); + let check = match self.on_toggle { + Some(on_toggle) => check.on_change(move |done, window, cx| on_toggle(done, window, cx)), + None => check, + }; + div().flex().items_center().gap_2().child(check).child( + div() + .flex_1() + .min_w_0() + .when(self.done, |label| { + label.line_through().text_color(colors.fg_subtle) + }) + .child(self.label), + ) + } +} + +/// To-dos under a title, with how many are done as a count and a bar. +#[derive(IntoElement)] +pub struct Checklist { + id: ElementId, + title: SharedString, + items: Vec<(SharedString, bool)>, + on_toggle: Option, +} + +impl Checklist { + pub fn new( + id: impl Into, + title: impl Into, + items: impl IntoIterator, bool)>, + ) -> Self { + Self { + id: id.into(), + title: title.into(), + items: items + .into_iter() + .map(|(label, done)| (label.into(), done)) + .collect(), + on_toggle: None, + } + } + + /// Tells which item changed, by index, and whether it is now done. + pub fn on_toggle( + mut self, + handler: impl Fn(usize, bool, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_toggle = Some(Rc::new(handler)); + self + } +} + +impl RenderOnce for Checklist { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let theme = cx.theme(); + let done = self.items.iter().filter(|(_, done)| *done).count(); + let total = self.items.len(); + let share = if total == 0 { + 0.0 + } else { + done as f32 / total as f32 + }; + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .flex() + .justify_between() + .child(div().font_weight(FontWeight::MEDIUM).child(self.title)) + .child( + tabular( + div() + .text_size(theme.text_size(TextSize::Sm)) + .text_color(theme.colors.fg_muted), + ) + .child(format!("{done} of {total}")), + ), + ) + .child(ProgressBar::new((self.id.clone(), "done"), share)) + .children( + self.items + .into_iter() + .enumerate() + .map(|(ix, (label, is_done))| { + let item = + TodoItem::new((self.id.clone(), format!("item-{ix}")), label, is_done); + match &self.on_toggle { + Some(on_toggle) => { + let on_toggle = on_toggle.clone(); + item.on_toggle(move |done, window, cx| { + on_toggle(ix, done, window, cx) + }) + } + None => item, + } + }), + ) + } +} diff --git a/src/documents/toolbar.rs b/src/documents/toolbar.rs new file mode 100644 index 0000000..2e9e98b --- /dev/null +++ b/src/documents/toolbar.rs @@ -0,0 +1,121 @@ +use gpui::{App, ElementId, Entity, IntoElement, Keystroke, ParentElement, RenderOnce, Window}; + +use super::format::{Format, active, format}; +use crate::{ + buttons::{ToggleButton, ToggleItem, shortcut_text}, + forms::{Run, TextInput}, + primitives::IconName, + shell::{Toolbar, ToolbarGroup, ToolbarSeparator}, + theme::ControlSize, +}; + +/// A tool: the format it applies, its icon, its name and its key. +type Tool = (Format, IconName, &'static str, Option<&'static str>); + +/// The inline styles, which the floating toolbar shows too. +pub(crate) const INLINE: &[Tool] = &[ + (Format::Bold, IconName::Bold, "Bold", Some("secondary-b")), + ( + Format::Italic, + IconName::Italic, + "Italic", + Some("secondary-i"), + ), + ( + Format::Strike, + IconName::Strikethrough, + "Strikethrough", + None, + ), + (Format::Code, IconName::Code, "Code", None), + (Format::Link, IconName::Link, "Link", Some("secondary-k")), +]; + +const HEADINGS: &[Tool] = &[ + (Format::Heading(1), IconName::Heading1, "Heading 1", None), + (Format::Heading(2), IconName::Heading2, "Heading 2", None), + (Format::Heading(3), IconName::Heading3, "Heading 3", None), +]; + +const BLOCKS: &[Tool] = &[ + (Format::Quote, IconName::Quote, "Quote", None), + (Format::Bullet, IconName::List, "Bulleted list", None), + ( + Format::Numbered, + IconName::ListOrdered, + "Numbered list", + None, + ), + (Format::Task, IconName::ListTodo, "To-do list", None), +]; + +/// One tool as a toggle: pressed while `on`; a press applies its format, or opens the link editor. +pub(crate) fn tool( + id: &ElementId, + field: &Entity, + (format_of, icon, name, key): Tool, + on: bool, + on_link: Option, +) -> ToggleButton { + let tip = match key { + Some(key) => { + let stroke = Keystroke::parse(key).expect("a tool's key parses"); + format!("{name} {}", shortcut_text(&stroke)) + } + None => name.to_string(), + }; + let field = field.clone(); + ToggleButton::new( + (id.clone(), name), + ToggleItem::new(name).icon(icon).tooltip(tip), + on, + ) + .size(ControlSize::Sm) + .on_toggle(move |_, window, cx| match (&on_link, format_of) { + (Some(open), Format::Link) => open(window, cx), + _ => format(&field, format_of, cx), + }) +} + +/// The formats a markdown field takes, in a row above it: inline styles, headings, quote and lists. A tool shows pressed while the selection holds its format; a press applies it and leaves focus in the field. +#[derive(IntoElement)] +pub struct FixedFormatToolbar { + id: ElementId, + field: Entity, + on_link: Option, +} + +impl FixedFormatToolbar { + pub fn new(id: impl Into, field: &Entity) -> Self { + Self { + id: id.into(), + field: field.clone(), + on_link: None, + } + } + + /// The link tool opens the owner's link editor instead of writing `[text]()`. + pub fn on_link(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { + self.on_link = Some(std::rc::Rc::new(handler)); + self + } +} + +impl RenderOnce for FixedFormatToolbar { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let input = self.field.read(cx); + let pressed = active(input.text(), input.selection()); + let group = |tools: &[Tool]| { + ToolbarGroup::new().children(tools.iter().map(|tool_of| { + let on = pressed.contains(&tool_of.0); + tool(&self.id, &self.field, *tool_of, on, self.on_link.clone()) + })) + }; + Toolbar::new() + .child(group(INLINE)) + .child(ToolbarSeparator) + .child(group(HEADINGS)) + .child(ToolbarSeparator) + .child(group(BLOCKS)) + } +} diff --git a/src/documents/tree.rs b/src/documents/tree.rs new file mode 100644 index 0000000..aebad3b --- /dev/null +++ b/src/documents/tree.rs @@ -0,0 +1,319 @@ +use std::ops::Range; + +use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Parser, Tag, TagEnd}; + +use super::markdown::options; + +/// How a stretch of inline text prints. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Style { + Strong, + Emphasis, + Strike, + Code, + Link, + Math, + Footnote, +} + +/// Inline content: its text, the styled stretches of it, and the address of each link, in order. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Inline { + pub text: String, + pub styles: Vec<(Range, Style)>, + pub links: Vec<(Range, String)>, +} + +/// A block of a markdown document. +#[derive(Clone, Debug, PartialEq)] +pub enum Node { + Paragraph(Inline), + Heading(u8, Inline), + Quote(Vec), + Code { + language: Option, + text: String, + }, + /// Items with their blocks, numbered from `start` when ordered, each with its checkbox when a task. + List { + start: Option, + items: Vec<(Option, Vec)>, + }, + Table { + aligns: Vec, + head: Vec, + rows: Vec>, + }, + Rule, + Math(String), + Image { + alt: String, + url: String, + }, + Footnote { + label: String, + body: Vec, + }, +} + +/// Parses into nodes as it goes: a stack of open containers and the inline text being built. +#[derive(Default)] +struct Builder { + stack: Vec<(Frame, Vec)>, + inline: Inline, + open: Vec<(usize, Style)>, + link: Vec<(usize, String)>, + image: Option<(String, String)>, + code: Option<(Option, String)>, + head: Vec, + row: Vec, + rows: Vec>, +} + +enum Frame { + Quote, + List(Option, Vec<(Option, Vec)>), + Item(Option), + Table(Vec), + Footnote(String), +} + +impl Builder { + fn take_inline(&mut self) -> Inline { + std::mem::take(&mut self.inline) + } +} + +/// `markdown` as blocks. +pub fn parse(markdown: &str) -> Vec { + let mut built = Builder::default(); + let mut out: Vec = Vec::new(); + for event in Parser::new_ext(markdown, options()) { + match event { + Event::Start(Tag::Paragraph | Tag::Heading { .. } | Tag::TableCell) => {} + Event::End(TagEnd::Paragraph) => { + let text = built.take_inline(); + if !text.text.is_empty() { + push(&mut built, &mut out, Node::Paragraph(text)); + } + } + Event::End(TagEnd::Heading(level)) => { + let text = built.take_inline(); + push(&mut built, &mut out, Node::Heading(depth(level), text)); + } + Event::Start(Tag::BlockQuote(_)) => built.stack.push((Frame::Quote, Vec::new())), + Event::Start(Tag::List(start)) => { + flush(&mut built, &mut out); + built + .stack + .push((Frame::List(start, Vec::new()), Vec::new())); + } + Event::Start(Tag::Item) => built.stack.push((Frame::Item(None), Vec::new())), + Event::TaskListMarker(done) => { + if let Some((Frame::Item(task), _)) = built.stack.last_mut() { + *task = Some(done); + } + } + Event::End(TagEnd::Item) => { + flush(&mut built, &mut out); + let (frame, nodes) = built.stack.pop().expect("an item was open"); + let Frame::Item(task) = frame else { + unreachable!("an item closes an item") + }; + if let Some((Frame::List(_, items), _)) = built.stack.last_mut() { + items.push((task, nodes)); + } + } + Event::End(TagEnd::List(_)) => { + let (frame, _) = built.stack.pop().expect("a list was open"); + let Frame::List(start, items) = frame else { + unreachable!("a list closes a list") + }; + push(&mut built, &mut out, Node::List { start, items }); + } + Event::End(TagEnd::BlockQuote(_)) => { + let (_, nodes) = built.stack.pop().expect("a quote was open"); + push(&mut built, &mut out, Node::Quote(nodes)); + } + Event::Start(Tag::FootnoteDefinition(label)) => built + .stack + .push((Frame::Footnote(label.to_string()), Vec::new())), + Event::End(TagEnd::FootnoteDefinition) => { + let (frame, body) = built.stack.pop().expect("a footnote was open"); + let Frame::Footnote(label) = frame else { + unreachable!("a footnote closes a footnote") + }; + push(&mut built, &mut out, Node::Footnote { label, body }); + } + Event::Start(Tag::CodeBlock(kind)) => { + let language = match kind { + CodeBlockKind::Fenced(name) if !name.is_empty() => Some(name.to_string()), + _ => None, + }; + built.code = Some((language, String::new())); + } + Event::End(TagEnd::CodeBlock) => { + let (language, text) = built.code.take().expect("a code block was open"); + push( + &mut built, + &mut out, + Node::Code { + language, + text: text.trim_end_matches('\n').to_string(), + }, + ); + } + Event::Start(Tag::Table(aligns)) => { + built.stack.push((Frame::Table(aligns), Vec::new())) + } + Event::End(TagEnd::TableCell) => { + let cell = built.take_inline(); + built.row.push(cell); + } + Event::End(TagEnd::TableHead) => built.head = std::mem::take(&mut built.row), + Event::End(TagEnd::TableRow) => { + let row = std::mem::take(&mut built.row); + built.rows.push(row); + } + Event::End(TagEnd::Table) => { + let (frame, _) = built.stack.pop().expect("a table was open"); + let Frame::Table(aligns) = frame else { + unreachable!("a table closes a table") + }; + let (head, rows) = ( + std::mem::take(&mut built.head), + std::mem::take(&mut built.rows), + ); + push(&mut built, &mut out, Node::Table { aligns, head, rows }); + } + Event::Rule => push(&mut built, &mut out, Node::Rule), + Event::DisplayMath(math) => push(&mut built, &mut out, Node::Math(math.to_string())), + Event::Start(Tag::Image { dest_url, .. }) => { + built.image = Some((String::new(), dest_url.to_string())) + } + Event::End(TagEnd::Image) => { + let (alt, url) = built.image.take().expect("an image was open"); + push(&mut built, &mut out, Node::Image { alt, url }); + } + Event::Start(Tag::Strong) => built.open.push((built.inline.text.len(), Style::Strong)), + Event::Start(Tag::Emphasis) => { + built.open.push((built.inline.text.len(), Style::Emphasis)) + } + Event::Start(Tag::Strikethrough) => { + built.open.push((built.inline.text.len(), Style::Strike)) + } + Event::End(TagEnd::Strong | TagEnd::Emphasis | TagEnd::Strikethrough) => { + let (start, style) = built.open.pop().expect("a style closes after it opens"); + let end = built.inline.text.len(); + built.inline.styles.push((start..end, style)); + } + Event::Start(Tag::Link { dest_url, .. }) => built + .link + .push((built.inline.text.len(), dest_url.to_string())), + Event::End(TagEnd::Link) => { + let (start, url) = built.link.pop().expect("a link closes after it opens"); + let range = start..built.inline.text.len(); + built.inline.styles.push((range.clone(), Style::Link)); + built.inline.links.push((range, url)); + } + Event::Text(text) => match (&mut built.code, &mut built.image) { + (Some((_, code)), _) => code.push_str(&text), + (None, Some((alt, _))) => alt.push_str(&text), + (None, None) => built.inline.text.push_str(&text), + }, + Event::Code(code) => styled(&mut built.inline, &code, Style::Code), + Event::InlineMath(math) => styled(&mut built.inline, &math, Style::Math), + Event::FootnoteReference(label) => { + styled(&mut built.inline, &format!("[{label}]"), Style::Footnote) + } + Event::SoftBreak => built.inline.text.push(' '), + Event::HardBreak => built.inline.text.push('\n'), + _ => {} + } + } + flush(&mut built, &mut out); + out +} + +fn styled(inline: &mut Inline, text: &str, style: Style) { + let start = inline.text.len(); + inline.text.push_str(text); + inline.styles.push((start..inline.text.len(), style)); +} + +fn depth(level: HeadingLevel) -> u8 { + level as u8 +} + +/// A finished node goes into the open container, or the document. +fn push(built: &mut Builder, out: &mut Vec, node: Node) { + match built.stack.last_mut() { + Some((_, nodes)) => nodes.push(node), + None => out.push(node), + } +} + +/// Text left from a tight list item becomes its paragraph. +fn flush(built: &mut Builder, out: &mut Vec) { + if !built.inline.text.is_empty() { + let text = built.take_inline(); + push(built, out, Node::Paragraph(text)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn blocks_nest_as_written() { + let nodes = parse( + "# Lift\n\nBlend **toward** [white](https://ely.dev).\n\n- [x] clamp\n- [ ] gamma\n\n> quoted\n\n```rust\nfn main() {}\n```\n\n---\n", + ); + assert!(matches!(&nodes[0], Node::Heading(1, text) if text.text == "Lift")); + let Node::Paragraph(line) = &nodes[1] else { + panic!("a paragraph, not {:?}", nodes[1]) + }; + assert_eq!(line.text, "Blend toward white."); + assert_eq!(line.styles, [(6..12, Style::Strong), (13..18, Style::Link)]); + assert_eq!(line.links, [(13..18, "https://ely.dev".to_string())]); + let Node::List { start: None, items } = &nodes[2] else { + panic!("a list, not {:?}", nodes[2]) + }; + assert_eq!( + items.iter().map(|(task, _)| *task).collect::>(), + [Some(true), Some(false)] + ); + assert!(matches!(&items[0].1[0], Node::Paragraph(text) if text.text == "clamp")); + assert!(matches!(&nodes[3], Node::Quote(inner) if inner.len() == 1)); + assert_eq!( + nodes[4], + Node::Code { + language: Some("rust".into()), + text: "fn main() {}".into() + } + ); + assert_eq!(nodes[5], Node::Rule); + } + + #[test] + fn display_math_stands_alone() { + assert_eq!(parse("$$c = 1$$"), [Node::Math("c = 1".into())]); + } + + #[test] + fn tables_keep_their_cells_and_alignment() { + let nodes = parse("| Name | Lift |\n|:--|--:|\n| accent | 0.5 |\n"); + let Node::Table { aligns, head, rows } = &nodes[0] else { + panic!("a table, not {:?}", nodes[0]) + }; + assert_eq!(aligns, &[Alignment::Left, Alignment::Right]); + assert_eq!( + head.iter() + .map(|cell| cell.text.as_str()) + .collect::>(), + ["Name", "Lift"] + ); + assert_eq!(rows[0][1].text, "0.5"); + } +} diff --git a/src/forms/code.rs b/src/forms/code.rs index 1bf2eeb..634e26c 100644 --- a/src/forms/code.rs +++ b/src/forms/code.rs @@ -83,13 +83,7 @@ pub(crate) fn lex(source: &str) -> Vec<(Range, Kind)> { fn painted(spans: impl Iterator, Hsla)>) -> Vec<(Range, Highlight)> { spans - .map(|(range, color)| { - let highlight = Highlight { - color, - background: None, - }; - (range, highlight) - }) + .map(|(range, color)| (range, Highlight::new(color))) .collect() } diff --git a/src/forms/expression.rs b/src/forms/expression.rs index c6f3cba..dc6777f 100644 --- a/src/forms/expression.rs +++ b/src/forms/expression.rs @@ -237,13 +237,7 @@ pub fn expression_highlights(source: &str, cx: &App) -> Vec<(Range, Highl Token::Op(_) => syntax.operator, Token::Open | Token::Close | Token::Comma => syntax.punctuation, }; - ( - range.clone(), - Highlight { - color, - background: None, - }, - ) + (range.clone(), Highlight::new(color)) }) .collect() } diff --git a/src/forms/glyphs.rs b/src/forms/glyphs.rs index 6878f68..0f45f7e 100644 --- a/src/forms/glyphs.rs +++ b/src/forms/glyphs.rs @@ -338,7 +338,7 @@ impl RenderOnce for IconPicker { } /// Emoji whose name or shortcode holds `query`, a lowercase string; all for an empty one. -fn emoji_found(query: &str) -> Vec<&'static emojis::Emoji> { +pub(crate) fn emoji_found(query: &str) -> Vec<&'static emojis::Emoji> { emojis::iter() .filter(|emoji| { query.is_empty() diff --git a/src/forms/mention.rs b/src/forms/mention.rs index fc9af5f..72d2b78 100644 --- a/src/forms/mention.rs +++ b/src/forms/mention.rs @@ -1,13 +1,13 @@ use std::{ops::Range, rc::Rc}; use gpui::{ - App, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce, - SharedString, Styled, Window, div, prelude::*, + App, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce, + SharedString, Stateful, Styled, Window, div, prelude::*, }; use super::{ Highlight, Input, TextInput, - options::{Choice, Popup}, + options::{Choice, Pick, Popup}, text::{Down, Enter, Up}, }; use crate::theme::ActiveTheme; @@ -72,8 +72,8 @@ pub fn mention_highlights(text: &str, cx: &App) -> Vec<(Range, Highlight) ( range, Highlight { - color: link, background: Some(link.opacity(0.08)), + ..Highlight::new(link) }, ) }) @@ -86,6 +86,94 @@ struct Picking { dismissed: Option, } +/// Rows offered at a trigger in a field's text, and what a pick does: arrows choose, Enter or a press picks, Escape dismisses until the trigger ends. +pub(crate) struct Suggestions { + pub id: ElementId, + pub state: Entity, + /// Where the trigger being typed starts. + pub trigger: Option, + pub rows: Vec, + pub pick: Pick, +} + +impl Suggestions { + /// Wraps the element that shows the field, with the rows under the trigger while it is typed. + pub fn wrap(self, field: impl IntoElement, window: &mut Window, cx: &mut App) -> Stateful
{ + let picking = window.use_keyed_state(self.id.clone(), cx, |_, _| Picking::default()); + let input = self.state.read(cx); + let focused = input.focus().is_focused(window); + let anchor = self.trigger.and_then(|ix| input.bounds_for(ix)); + let dismissed = dismissal(picking.read(cx).dismissed, self.trigger); + if dismissed != picking.read(cx).dismissed { + picking.update(cx, |picking, _| picking.dismissed = dismissed); + } + let open = focused + && !self.rows.is_empty() + && self.trigger.is_some_and(|ix| dismissed != Some(ix)); + let count = self.rows.len(); + let highlighted = picking.read(cx).highlighted.min(count.saturating_sub(1)); + log::debug!( + "suggestions {:?}: open {open}, {count} rows, anchor {anchor:?}", + self.id + ); + let (up, down, escape) = (picking.clone(), picking.clone(), picking); + let (enter, trigger) = (self.pick.clone(), self.trigger); + div() + .id(self.id) + .relative() + .w_full() + .capture_action(move |_: &Up, _, cx| { + if open { + cx.stop_propagation(); + up.update(cx, |picking, cx| { + picking.highlighted = (picking.highlighted + count - 1) % count; + cx.notify(); + }); + } + }) + .capture_action(move |_: &Down, _, cx| { + if open { + cx.stop_propagation(); + down.update(cx, |picking, cx| { + picking.highlighted = (picking.highlighted + 1) % count; + cx.notify(); + }); + } + }) + .capture_action(move |_: &Enter, window, cx| { + if open { + cx.stop_propagation(); + enter(highlighted, window, cx); + } + }) + .on_key_down(move |event, _, cx| { + if open && event.keystroke.key == "escape" { + cx.stop_propagation(); + escape.update(cx, |picking, cx| { + picking.dismissed = trigger; + cx.notify(); + }); + } + }) + .child(field) + .when_some(anchor.filter(|_| open), |field, anchor| { + field.child( + Popup { + id: "suggestions".into(), + anchor, + rows: &self.rows, + highlighted: Some(highlighted), + checked: None, + pick: self.pick, + dismiss: None, + scroll: None, + } + .render(window, cx), + ) + }) + } +} + /// A field where `@` or `#` opens suggestions at the caret. Arrows choose; Enter picks. #[derive(IntoElement)] pub struct MentionInput { @@ -122,46 +210,34 @@ impl MentionInput { } } +/// The handles after `trigger` that hold the typed query, a few at most. +pub(crate) fn handles_matching(handles: &[SharedString], query: &str) -> Vec { + let query = query.to_lowercase(); + handles + .iter() + .filter(|name| name.to_lowercase().contains(&query)) + .take(SHOWN) + .cloned() + .collect() +} + impl RenderOnce for MentionInput { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let picking = window.use_keyed_state(self.id.clone(), cx, |_, _| Picking::default()); let input = self.state.read(cx); let (text, caret) = (input.text().to_string(), input.cursor()); - let focused = input.focus().is_focused(window); let marks: Vec = self.triggers.iter().map(|(trigger, _)| *trigger).collect(); let active = active_trigger(&text, caret, &marks); - let anchor = active.and_then(|(ix, _)| input.bounds_for(ix)); let matches: Vec = active .map(|(ix, trigger)| { - let query = text[ix + trigger.len_utf8()..caret].to_lowercase(); let names = &self .triggers .iter() .find(|(mark, _)| *mark == trigger) .expect("from triggers") .1; - names - .iter() - .filter(|name| name.to_lowercase().contains(&query)) - .take(SHOWN) - .cloned() - .collect() + handles_matching(names, &text[ix + trigger.len_utf8()..caret]) }) .unwrap_or_default(); - let dismissed = dismissal(picking.read(cx).dismissed, active.map(|(ix, _)| ix)); - if dismissed != picking.read(cx).dismissed { - picking.update(cx, |picking, _| picking.dismissed = dismissed); - } - let open = - focused && !matches.is_empty() && active.is_some_and(|(ix, _)| dismissed != Some(ix)); - let highlighted = picking - .read(cx) - .highlighted - .min(matches.len().saturating_sub(1)); - log::debug!( - "mention input: open {open}, {} matches, caret {anchor:?}", - matches.len() - ); let rows: Vec = matches .iter() .map(|name| { @@ -171,8 +247,8 @@ impl RenderOnce for MentionInput { Choice::new(name.clone(), label) }) .collect(); - let insert = { - let (state, matches) = (self.state.clone(), matches.clone()); + let insert: Pick = { + let state = self.state.clone(); Rc::new(move |pick: usize, _: &mut Window, cx: &mut App| { let Some((ix, trigger)) = active else { return; @@ -185,66 +261,14 @@ impl RenderOnce for MentionInput { }); }) }; - let count = matches.len(); - let (up, down, enter, escape) = ( - picking.clone(), - picking.clone(), - insert.clone(), - picking.clone(), - ); - div() - .id(self.id) - .relative() - .w_full() - .capture_action(move |_: &Up, _, cx| { - if open { - cx.stop_propagation(); - up.update(cx, |picking, cx| { - picking.highlighted = (picking.highlighted + count - 1) % count; - cx.notify(); - }); - } - }) - .capture_action(move |_: &Down, _, cx| { - if open { - cx.stop_propagation(); - down.update(cx, |picking, cx| { - picking.highlighted = (picking.highlighted + 1) % count; - cx.notify(); - }); - } - }) - .capture_action(move |_: &Enter, window, cx| { - if open { - cx.stop_propagation(); - enter(highlighted, window, cx); - } - }) - .on_key_down(move |event, _, cx| { - if open && event.keystroke.key == "escape" { - cx.stop_propagation(); - escape.update(cx, |picking, cx| { - picking.dismissed = active.map(|(ix, _)| ix); - cx.notify(); - }); - } - }) - .child(Input::new(&self.state)) - .when_some(anchor.filter(|_| open), |field, anchor| { - field.child( - Popup { - id: "mentions".into(), - anchor, - rows: &rows, - highlighted: Some(highlighted), - checked: None, - pick: insert, - dismiss: None, - scroll: None, - } - .render(window, cx), - ) - }) + Suggestions { + id: self.id, + state: self.state.clone(), + trigger: active.map(|(ix, _)| ix), + rows, + pick: insert, + } + .wrap(Input::new(&self.state), window, cx) } } diff --git a/src/forms/mod.rs b/src/forms/mod.rs index d9dc9f4..689146c 100644 --- a/src/forms/mod.rs +++ b/src/forms/mod.rs @@ -64,6 +64,7 @@ pub use date::{ }; pub use expression::{ExpressionInput, evaluate, expression_highlights}; pub use files::{DropZone, FileInput}; +pub(crate) use glyphs::emoji_found; pub use glyphs::{EmojiPicker, IconPicker}; pub use group::{InputAddon, InputGroup}; pub use hotkey::HotkeyInput; @@ -74,6 +75,7 @@ pub use knob::Knob; pub use listbox::ListBox; pub use masked::{COUNTRIES, MaskedInput, PhoneInput}; pub use mention::{MentionInput, mention_highlights}; +pub(crate) use mention::{Suggestions, active_trigger, handles_matching}; pub use multi::MultiSelect; pub use number::{NumberInput, ScrubInput}; pub use options::Choice; @@ -95,7 +97,9 @@ pub use structure::{ }; pub use switch::Switch; pub use tags::TagInput; -pub(crate) use text::{Down, Enter, History, Submit, Up, bind_keys, from_utf16, to_utf16}; +pub(crate) use text::{ + Backspace, Down, Enter, History, Redo, Submit, Undo, Up, bind_keys, from_utf16, to_utf16, +}; pub use text::{Highlight, InputEvent, TextInput}; pub use transfer::TransferList; pub use unit::UnitInput; diff --git a/src/forms/pattern.rs b/src/forms/pattern.rs index 1cedab4..37d67b6 100644 --- a/src/forms/pattern.rs +++ b/src/forms/pattern.rs @@ -90,13 +90,7 @@ pub fn regex_highlights(pattern: &str, cx: &App) -> Vec<(Range, Highlight Part::Anchor => syntax.constant, Part::Alternation => syntax.operator, }; - ( - range, - Highlight { - color, - background: None, - }, - ) + (range, Highlight::new(color)) }) .collect() } diff --git a/src/forms/text/element.rs b/src/forms/text/element.rs index 6098f99..79c77c8 100644 --- a/src/forms/text/element.rs +++ b/src/forms/text/element.rs @@ -1,11 +1,11 @@ use gpui::{ App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element, ElementId, - ElementInputHandler, Entity, GlobalElementId, Hsla, InspectorElementId, IntoElement, LayoutId, - MouseMoveEvent, Pixels, Point, SharedString, Style, TextAlign, TextRun, UnderlineStyle, Window, - WrappedLine, fill, point, relative, size, + ElementInputHandler, Entity, FontStyle, GlobalElementId, Hsla, InspectorElementId, IntoElement, + LayoutId, MouseMoveEvent, Pixels, Point, SharedString, StrikethroughStyle, Style, TextAlign, + TextRun, UnderlineStyle, Window, WrappedLine, fill, point, relative, size, }; -use super::{Layout, TextInput}; +use super::{Highlight, Layout, TextInput}; use crate::theme::ActiveTheme; pub(crate) struct TextElement { @@ -54,11 +54,11 @@ fn plain_run(len: usize, window: &Window, color: Hsla) -> TextRun { } } -/// Splits `len` bytes into runs: highlights and the marked range over a base color. +/// Splits `len` bytes into runs: highlights, and the underlined marked range, over a base run. fn runs( len: usize, base: TextRun, - spans: Vec<(std::ops::Range, Hsla, Option, bool)>, + spans: Vec<(std::ops::Range, Highlight, bool)>, thickness: Pixels, ) -> Vec { let mut cuts: Vec = vec![0, len]; @@ -74,15 +74,27 @@ fn runs( len: end - start, ..base.clone() }; - for (range, color, background, underline) in &spans { + for (range, highlight, underline) in &spans { if range.start <= start && end <= range.end { - run.color = *color; - if background.is_some() { - run.background_color = *background; + run.color = highlight.color; + if highlight.background.is_some() { + run.background_color = highlight.background; + } + if let Some(weight) = highlight.weight { + run.font.weight = weight; + } + if highlight.italic { + run.font.style = FontStyle::Italic; + } + if highlight.strike { + run.strikethrough = Some(StrikethroughStyle { + thickness, + color: Some(highlight.color), + }); } if *underline { run.underline = Some(UnderlineStyle { - color: Some(*color), + color: Some(highlight.color), thickness, wavy: false, }); @@ -95,6 +107,34 @@ fn runs( .collect() } +/// What to show, whether it is the placeholder, and its runs, the same for measure and paint. +fn shown_runs(input: &TextInput, window: &Window, cx: &App) -> (String, bool, Vec) { + let theme = cx.theme(); + let (text, placeholder) = shown(input); + let color = window.text_style().color; + let base = plain_run( + text.len(), + window, + if placeholder { + theme.colors.fg_subtle + } else { + color + }, + ); + let mut spans = Vec::new(); + if !placeholder { + for (range, highlight) in input.highlights(cx) { + spans.push((range, highlight, false)); + } + if let Some(marked) = input.marked() { + let marked = input.display_offset(marked.start)..input.display_offset(marked.end); + spans.push((marked, Highlight::new(color), true)); + } + } + let runs = runs(text.len(), base, spans, theme.underline_thickness()); + (text, placeholder, runs) +} + /// Starts of the hard lines in `text`. fn line_starts(text: &str) -> Vec { std::iter::once(0) @@ -129,9 +169,8 @@ impl Element for TextElement { style.size.height = line_height.into(); return (window.request_layout(style, [], cx), ()); }; - let (text, _) = shown(input); + let (text, _, runs) = shown_runs(input, window, cx); let font_size = window.text_style().font_size.to_pixels(window.rem_size()); - let run = plain_run(text.len(), window, Hsla::default()); let layout = window.request_measured_layout(style, move |known, available, window, _| { let width = known.width.or(match available.width { AvailableSpace::Definite(width) => Some(width), @@ -142,7 +181,7 @@ impl Element for TextElement { .shape_text( SharedString::from(text.clone()), font_size, - std::slice::from_ref(&run), + &runs, width, None, ) @@ -169,47 +208,16 @@ impl Element for TextElement { cx: &mut App, ) -> Prepaint { let theme = cx.theme(); - let (selection_color, caret_color, subtle) = ( - theme.colors.selection, - theme.colors.focus, - theme.colors.fg_subtle, - ); + let (selection_color, caret_color) = (theme.colors.selection, theme.colors.focus); let caret_width = theme.caret_width().to_pixels(window.rem_size()); - let thickness = theme.underline_thickness(); let input = self.input.read(cx); - let (text, placeholder) = shown(input); - let base = plain_run( - text.len(), - window, - if placeholder { - subtle - } else { - window.text_style().color - }, - ); - let mut spans = Vec::new(); - if !placeholder { - for (range, highlight) in input.highlights(cx) { - spans.push((range, highlight.color, highlight.background, false)); - } - if let Some(marked) = input.marked() { - let color = window.text_style().color; - let marked = input.display_offset(marked.start)..input.display_offset(marked.end); - spans.push((marked, color, None, true)); - } - } + let (text, placeholder, runs) = shown_runs(input, window, cx); let font_size = window.text_style().font_size.to_pixels(window.rem_size()); let line_height = window.line_height(); let wrap = input.rows().map(|_| bounds.size.width); let shaped = window .text_system() - .shape_text( - text.clone().into(), - font_size, - &runs(text.len(), base, spans, thickness), - wrap, - None, - ) + .shape_text(text.clone().into(), font_size, &runs, wrap, None) .expect("text input shaping failed"); let lines: Vec<(usize, WrappedLine)> = line_starts(&text).into_iter().zip(shaped).collect(); let focused = input.focus().is_focused(window); @@ -400,7 +408,55 @@ fn selection_rects( #[cfg(test)] mod tests { - use super::line_starts; + use gpui::{FontWeight, font, hsla, px}; + + use super::*; + + #[test] + fn a_highlight_sets_weight_slant_and_strike_on_its_runs() { + let base = TextRun { + len: 3, + font: font("Inter"), + color: hsla(0., 0., 0., 1.), + background_color: None, + underline: None, + strikethrough: None, + }; + let bold = Highlight { + weight: Some(FontWeight::SEMIBOLD), + ..Highlight::new(hsla(0., 0., 0.5, 1.)) + }; + let slanted = Highlight { + italic: true, + strike: true, + ..Highlight::new(hsla(0., 0., 0.2, 1.)) + }; + let split = runs( + 3, + base, + vec![(0..2, bold, false), (1..3, slanted, false)], + px(1.), + ); + let styles: Vec<_> = split + .iter() + .map(|run| { + ( + run.len, + run.font.weight, + run.font.style, + run.strikethrough.is_some(), + ) + }) + .collect(); + assert_eq!( + styles, + [ + (1, FontWeight::SEMIBOLD, FontStyle::Normal, false), + (1, FontWeight::SEMIBOLD, FontStyle::Italic, true), + (1, FontWeight::NORMAL, FontStyle::Italic, true), + ] + ); + } #[test] fn line_starts_follow_newlines() { diff --git a/src/forms/text/highlight.rs b/src/forms/text/highlight.rs new file mode 100644 index 0000000..c82ab34 --- /dev/null +++ b/src/forms/text/highlight.rs @@ -0,0 +1,24 @@ +use gpui::{FontWeight, Hsla}; + +/// Styles a span of the text: its color, an optional wash behind it, and its weight, slant and strike. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Highlight { + pub color: Hsla, + pub background: Option, + pub weight: Option, + pub italic: bool, + pub strike: bool, +} + +impl Highlight { + /// A span in `color`, its font as the field's. + pub fn new(color: Hsla) -> Self { + Self { + color, + background: None, + weight: None, + italic: false, + strike: false, + } + } +} diff --git a/src/forms/text/mod.rs b/src/forms/text/mod.rs index 004e4cb..64f3fda 100644 --- a/src/forms/text/mod.rs +++ b/src/forms/text/mod.rs @@ -1,18 +1,20 @@ mod actions; mod edit; mod element; +mod highlight; mod ime; use std::{ops::Range, rc::Rc, time::Duration}; use gpui::{ - App, Bounds, Context, EventEmitter, FocusHandle, Focusable, Hsla, Pixels, Point, SharedString, + App, Bounds, Context, EventEmitter, FocusHandle, Focusable, Pixels, Point, SharedString, Subscription, Task, Window, WrappedLine, }; -pub(crate) use actions::{Backspace, Down, Enter, Submit, Up, bind_keys}; +pub(crate) use actions::{Backspace, Down, Enter, Redo, Submit, Undo, Up, bind_keys}; use edit::Snapshot; pub(crate) use edit::{History, from_utf16, to_utf16}; +pub use highlight::Highlight; use crate::theme::ActiveTheme; @@ -26,13 +28,6 @@ pub enum InputEvent { Blur, } -/// Colors a span of the text: foreground, and an optional wash behind it. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Highlight { - pub color: Hsla, - pub background: Option, -} - type Highlighter = Rc Vec<(Range, Highlight)>>; type Filter = Rc bool>; pub(crate) type Fit = Rc, &str) -> (String, usize)>; diff --git a/src/lib.rs b/src/lib.rs index 055dfae..71b6a5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod buttons; pub mod charts; pub mod data_display; pub mod debug; +pub mod documents; pub mod editor; pub mod feedback; pub mod finance; @@ -44,4 +45,5 @@ pub fn init(cx: &mut App) { forms::bind_keys(cx); editor::bind_keys(cx); terminal::bind_keys(cx); + documents::bind_keys(cx); } diff --git a/src/primitives/icon.rs b/src/primitives/icon.rs index 3e75226..e29e157 100644 --- a/src/primitives/icon.rs +++ b/src/primitives/icon.rs @@ -146,6 +146,9 @@ icons! { HardDrive => "hard-drive", Hash => "hash", Heading => "heading", + Heading1 => "heading-1", + Heading2 => "heading-2", + Heading3 => "heading-3", Heart => "heart", History => "history", Hourglass => "hourglass", @@ -180,6 +183,7 @@ icons! { Maximize2 => "maximize-2", MemoryStick => "memory-stick", Menu => "menu", + Merge => "merge", MessageSquare => "message-square", MicOff => "mic-off", Mic => "mic", @@ -272,6 +276,7 @@ icons! { WholeWord => "whole-word", WifiOff => "wifi-off", Wifi => "wifi", + Workflow => "workflow", Wrench => "wrench", X => "x", Zap => "zap", diff --git a/tasks/ch21-30.md b/tasks/ch21-30.md index 9d6a5eb..93ee0b4 100644 --- a/tasks/ch21-30.md +++ b/tasks/ch21-30.md @@ -2,26 +2,26 @@ ## T21a Documents · Editing — `src/documents` -- [ ] RichTextEditor -- [ ] MarkdownEditor — WYSIWYG, source, split -- [ ] BlockEditor -- [ ] Block types — paragraph, heading, list, todo, quote, code, divider, table, image, video, embed, math, callout, toggle, columns, synced -- [ ] BlockHandle -- [ ] SlashMenu -- [ ] FloatingFormatToolbar → `overlays::FloatingToolbar` -- [ ] FixedFormatToolbar -- [ ] LinkEditor -- [ ] MentionMenu -- [ ] EmojiAutocomplete -- [ ] TableEditor -- [ ] ImageEditorBlock -- [ ] MathEditor / LatexBlock -- [ ] MermaidBlock / DiagramBlock — host renders the SVG -- [ ] CodeBlockEditor -- [ ] TodoItem / Checklist -- [ ] PlaceholderText -- [ ] WordCount / CharacterCount -- [ ] ReadingTime +- [x] RichTextEditor +- [x] MarkdownEditor — visual, source, split +- [x] BlockEditor +- [x] Block types — paragraph, heading, list, todo, quote, code, divider, table, image, video, embed, math, callout, toggle, columns, synced; video and embed open as cards the app plays or shows +- [x] BlockHandle → the block editor's handle, a `menus::OverflowMenu` that drags its block +- [x] SlashMenu → `/` in `BlockEditor` +- [x] FloatingFormatToolbar → `overlays::FloatingToolbar` +- [x] FixedFormatToolbar +- [x] LinkEditor +- [x] MentionMenu → `@` in `RichTextEditor` and `BlockEditor`; `forms::MentionInput` in a field +- [x] EmojiAutocomplete → a colon and two letters in `RichTextEditor` and `BlockEditor` +- [x] TableEditor → the block editor's table: rows and columns added and removed, cells merged across +- [x] ImageEditorBlock → the block editor's picture: resized by its edges, aligned, captioned +- [x] MathEditor / LatexBlock → the block editor's math block over `typography::Latex` +- [x] MermaidBlock / DiagramBlock — host renders the SVG → `BlockEditor::diagrams` +- [x] CodeBlockEditor → the block editor's code block +- [x] TodoItem / Checklist +- [x] PlaceholderText → each kind's hint in `BlockEditor` +- [x] WordCount / CharacterCount → `WordCount`, with `limit` for characters +- [x] ReadingTime ## T21b Documents · Reading