markdown_editor/lib.rs
1//! A block-based WYSIWYG Markdown editor for GPUI, built on gpui-kit's
2//! public editor host API (`gpui_kit::base::text::editor_host`).
3//!
4//! The design began as a spike in
5//! [longbridge/gpui-kit#3049](https://github.com/longbridge/gpui-kit/pull/3049)
6//! and [#3566](https://github.com/longbridge/gpui-kit/pull/3566); see
7//! `docs/design.md`. Call [`init`] once, after `gpui_kit::init`.
8//!
9//! - [`BlockEditorModel`]: the headless model. It has stable [`BlockId`]s,
10//! validated [`BlockPosition`]s, normalized inline mark runs, invertible
11//! steps grouped into transactions, undo coalescing, commands on nested
12//! blocks ([`EditorCommand`]), and a Markdown serializer.
13//! - [`BlockEditorState`]: the entity that owns a model, focus, IME and
14//! key bindings, and emits [`BlockEditorEvent`]s.
15//! - [`BlockEditor`]: the view. It renders through gpui-kit's own text
16//! renderer (a direct model-to-node projection) and culls off-screen blocks.
17//!
18//! ```ignore
19//! let scroll = ScrollHandle::new();
20//! let editor = cx.new(|cx| {
21//! let mut state = BlockEditorState::new(window, cx).with_source("# Hello");
22//! // Optional: keep the caret in view inside your scroll container.
23//! state.set_scroll_handle(scroll.clone());
24//! state
25//! });
26//! cx.subscribe(&editor, |_, editor, event: &BlockEditorEvent, cx| {
27//! if let BlockEditorEvent::Changed { .. } = event {
28//! let markdown = editor.read(cx).source();
29//! }
30//! });
31//! // in render:
32//! div().id("doc").overflow_y_scroll().track_scroll(&scroll)
33//! .child(BlockEditor::new(&editor))
34//! ```
35//!
36//! Keys: Enter, Backspace/Delete, arrows/Home/End (+Shift), Cmd/Ctrl+A, B, I,
37//! E, Z, Shift+Z, C, X, V, and Shift+V (plain paste); Tab/Shift+Tab indent
38//! list items; Cmd/Ctrl+Alt+0 makes the block (or each selected block) a
39//! paragraph. Input rules: `# `..`###### `, `- `, `* `, `+ `, `1. `.
40//! The scroll handle is optional: without it, blocks outside the visible
41//! area are still culled, but moving the caret never scrolls (a no-op).
42
43mod command;
44mod cull;
45mod direct;
46mod document;
47mod editor;
48mod element;
49mod frame;
50mod host;
51mod id;
52mod import;
53mod inline;
54mod input_rule;
55mod kind;
56mod markdown_source;
57mod marks;
58mod order;
59mod position;
60mod projection;
61mod selection;
62mod serialize;
63mod slice;
64mod state;
65mod step;
66mod style;
67mod support;
68pub(crate) mod table;
69mod table_ui;
70mod transaction;
71mod undo;
72
73#[cfg(test)]
74mod tests;
75
76pub mod component;
77
78pub use command::{CommandError, EditorCommand};
79pub use document::{BlockRef, DocumentRef};
80pub use editor::BlockEditorModel;
81pub use element::BlockEditor;
82pub use gpui_kit::base::text::editor_host::ColumnAlign;
83pub use input_rule::InputRule;
84pub use state::{BlockEditorEvent, BlockEditorState, ClipboardPolicy, PasteRequest, SubmitPolicy};
85pub use style::BlockEditorStyle;
86pub use table::{TableEdit, TableLine};
87
88/// Registers the editor's key bindings. Call once at startup, after `gpui_kit::init`.
89pub fn init(cx: &mut gpui::App) {
90 state::init(cx);
91}
92/// gpui-kit's own layers first (theme, global state), then the editor.
93#[cfg(test)]
94pub(crate) fn init_for_tests(cx: &mut gpui::App) {
95 gpui_kit::init(cx);
96 init(cx);
97}
98
99pub use id::BlockId;
100pub use kind::BlockKind;
101pub use marks::{MarkChange, MarkKind, MarkSet, MarkState};
102pub use position::{Affinity, BlockPosition, PositionError};
103pub use selection::{BlockSelection, SelectionError};
104pub use transaction::{EditOrigin, Transaction};
105
106pub use host::{CapturedRange, CapturedRangeError, SourceSnapshot};
107
108/// Whether image atoms may load resources while the editor presents them.
109/// `Placeholder` keeps the atomic document, source and selection but never
110/// resolves URLs, decodes data, reads paths or creates image elements.
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
112pub enum ImagePolicy {
113 #[default]
114 Load,
115 Placeholder,
116}