# gpui-kit-markdown-editor
A block-based WYSIWYG Markdown editor for [GPUI](https://www.gpui.rs), built on
[gpui-kit](https://github.com/longbridge/gpui-kit)'s public editor host API
(`gpui_kit::base::text::editor_host`). It edits real Markdown: paragraphs,
headings, nested lists, task lists, block quotes, fenced code blocks, tables,
links, and bold/italic/strike/inline-code marks. It has cross-block selection,
IME, undo/redo, input rules (`# `, `- `, ```` ``` ````), copy/paste as Markdown,
and culling of off-screen blocks.
<img width="826" height="418" alt="image" src="https://github.com/user-attachments/assets/27ca3783-93b6-4acb-a17e-8a111606d44b" />
## Usage
```toml
[dependencies]
gpui-kit = "0.8.0"
gpui-kit-markdown-editor = { git = "https://github.com/wabzqem/gpui-kit-markdown-editor" }
```
The library is imported as `markdown_editor`.
```rust
use gpui_kit::*;
use markdown_editor::{BlockEditor, BlockEditorEvent, BlockEditorState};
struct Notes {
editor: Entity<BlockEditorState>,
scroll: ScrollHandle,
}
impl Notes {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let scroll = ScrollHandle::new();
let editor = cx.new(|cx| {
let mut state =
BlockEditorState::new(window, cx).with_source("# Hello\n\nType **here**.");
// Optional: keep the caret in view inside your scroll container.
state.set_scroll_handle(scroll.clone());
state
});
cx.subscribe(&editor, |_, editor, event: &BlockEditorEvent, cx| {
if let BlockEditorEvent::Changed { .. } = event {
let _markdown = editor.read(cx).source();
}
})
.detach();
Self { editor, scroll }
}
}
impl Render for Notes {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.id("notes")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.scroll)
// `min_h_full` makes the whole window clickable, not only the text.
.child(BlockEditor::new(&self.editor).min_h_full().p_4())
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_kit::init(cx);
markdown_editor::init(cx); // key bindings; call after gpui_kit::init
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
cx.new(|cx| Notes::new(window, cx))
})
.expect("failed to open window");
});
}
```
A themed, content-sized wrapper lives in `markdown_editor::component`.
Images load by default. Use `ImagePolicy::Placeholder` to show a placeholder
instead and never resolve URLs, decode data or read paths.
## Features and keys
Cargo features forward to gpui-kit's syntax highlighting for fenced code
blocks; gpui-kit only highlights when one is on:
| `syntect` (default) | About 200 languages, no syntax tree |
| `tree-sitter` | Tree-sitter highlighting (takes precedence if both are on) |
Disable default features to build without highlighting.
Keys (Cmd instead of Ctrl on macOS): Enter, Backspace/Delete, arrows,
Home/End, Ctrl+Home/End (document start/end), Shift to select, Ctrl+A,
Ctrl+B/I/E (bold, italic, code), Ctrl+K (edit link), Ctrl+Alt+0 (normal
text: the block, or each selected block, becomes a paragraph; Backspace at the
start of a heading, a Backspace/Delete that empties it, and Enter in an empty
heading do the same), Ctrl+Z / Ctrl+Shift+Z
(or Ctrl+Y), Ctrl+C/X/V, Ctrl+Shift+V (plain paste), Tab/Shift+Tab (indent
list items, move between table cells), Shift+Enter (soft break, also in a
table cell). In a table (caret or cell selection): Ctrl+Alt+Left/Right insert
a column left/right, Ctrl+Alt+Up/Down insert a row above/below, and
Ctrl+Alt+Backspace deletes the selected column(s) when whole columns are
selected, otherwise the current row(s) (never the header row). Hovering a table
shows a grip above the column and left of the row; clicking it selects that
column / row and opens a menu (insert, delete, column alignment). Cmd/Ctrl-click a link emits `LinkClicked`; the editor never
opens a browser.
Drag to reorder (D22): drag a table or code block by its edge handle (the
strip left of it; the pointer becomes an open hand) and a line shows where it
will drop between top-level blocks. One undo step. Other blocks are not
draggable.
Text-editing keys (D21). Word deletes: Option+Backspace / Option+Delete on
macOS, Ctrl+Backspace / Ctrl+Delete elsewhere (word motion is Option+arrows /
Ctrl+arrows). macOS also has Cmd+Backspace / Cmd+Delete (delete to the start /
end of the visual line; in a code block, of the code line), Cmd+Left/Right and
Shift (visual line start / end, like Home/End), Option+Up/Down and Shift (block
start / end; Ctrl+Up/Down and Shift on Windows/Linux), and the Cocoa emacs keys Ctrl+A/E (and Shift), Ctrl+D/H (delete
forward / back) and Ctrl+K (delete to the line end; no kill ring). With a
selection, every delete key removes just the selection. At the start of a
block, word and line deletes act as Backspace (join, lift, heading to
paragraph); at its end, forward ones act as Delete. In a table cell they stay
in the cell and do nothing at its edges. A run of word deletes is one undo
step (separate from plain Backspace/Delete); a line delete is its own.
## Known limitations
- Copying across blocks writes Markdown and pastes back as blocks (headings,
lists, quotes, code, tables, tasks, marks). A table the selection touches is
copied whole, and a selection starting inside a nested list lifts the nested
items out instead of copying an empty parent item. Pasting a list directly
after another list joins them.
- Empty paragraphs have no Markdown form and disappear on save; list
tightness can change once on the first save, after which output is stable.
- A `BlockEditor` is as tall as its text. Give it `min_h_full()` (and padding
on the editor, not an outer wrapper) if the whole area should be clickable.
- Accessibility exposes the document as one multi-line text input plus a node
per laid-out top-level block; nested items, table cells and inline roles are
not exposed, and screen-reader behaviour is unverified.
- Culling covers top-level blocks and long top-level lists; nested long lists
and long tables are laid out in full.
- No `source_offset` mapping, no pluggable input-rule registry, and events
carry no change summaries yet.
- Developed and tested on Linux (under a virtual display). macOS and Windows
key bindings and IME are implemented but unverified here.
- Bold text only looks bold if the theme font has a bold face (the same as
gpui-kit's own `TextView`).
## Build and run
Linux needs the same system libraries as gpui-kit
(`script/install-linux.sh` in that repo). Then:
```sh
cargo run --example story
cargo test # window tests need a display; headless: xvfb-run -a cargo test
```
Everything builds from public dependencies only.
## Design
- [`docs/design.md`](docs/design.md): the editor design.
- [`docs/host-contract.md`](docs/host-contract.md): what the editor needs from the gpui-kit host API.
- [`docs/design-notes.md`](docs/design-notes.md): implementation notes from the spike.
## History and credits
The code began as the block editor spike in gpui-kit
([#3049](https://github.com/longbridge/gpui-kit/pull/3049),
[#3566](https://github.com/longbridge/gpui-kit/pull/3566)); its commit history
is preserved in this repository.
## Licence
Apache-2.0, the same as gpui-kit. See [LICENSE-APACHE](LICENSE-APACHE).