gpui-editor
A from-scratch, multi-line text editor for GPUI — the basis for Zorite’s note editor.
Host-agnostic: it depends only on gpui (+ unicode-segmentation), not on
gpui-component. It’s built directly on GPUI’s text primitives — an
EntityInputHandler for keyboard + IME input, shape_line for per-line text
shaping, and a custom Element that lays out and paints the lines, caret, and
selection.
Overview
Section titled “Overview”- Auto-grows to its content height (no inner scrollbar), so a host can stack many editors in one scroll view (e.g. a journal feed).
- Editing: insert / backspace / delete / newline, arrow + Home/End + word-wise navigation, visual-row up/down, copy / cut / paste, IME, undo / redo (coalesced), click + drag selection, double-click word / triple-click line.
- Soft-wrap with content-driven height.
- Spell-check squiggles: the host feeds in misspelled byte ranges
(
Diagnostic); a right-click menu offers replacements via a lazy provider. - Live-preview Markdown (“WYSIWYG”): with a
SyntaxStyleinstalled, the editor styles its own content as you type — headings (variable line height), bold / italic / strikethrough, inline code, links / wiki-links / tags, blockquotes, lists, task checkboxes, fenced code blocks, thematic rules, footnotes, reference links,<mark>— with the raw Markdown markers hidden and revealed only around the caret. - Block widgets: standalone images, file chips (e.g. PDF embeds), and mermaid diagrams render in place via host-supplied providers (raw source under the caret).
- Tables: rendered as a grid and edited in the cells; host-driven column alignment and row/column insert/delete.
Adding the dependency
Section titled “Adding the dependency”It’s a path/git crate (not on crates.io — gpui is a git-only dependency):
[dependencies]gpui-editor = { path = "crates/gpui-editor" } # or a git dependencygpui revision: this crate pins
gpui = { git = ".../zed" }with no rev and relies on the workspace’s single lockfile to unify everything onto onegpui. In a separate workspace, keep thegpuirev in lockstep with this crate’s lock or you’ll get twogpuiversions in one build (won’t compile).
Quick start
Section titled “Quick start”use gpui::*;use gpui_editor::{EditorState, EditorEvent};
// 1. Once at startup, bind the editing keys (scoped to the editor's key context).gpui_editor::bind_keys(cx);
// 2. Create the editor entity.let editor = cx.new(|cx| { EditorState::new(window, cx) .with_placeholder("Type here…") .with_text("# Hello\n\nSome **markdown**.")});
// 3. Focus it to start editing.editor.update(cx, |ed, cx| ed.focus(window, cx));
// 4. React to edits (e.g. save, re-run spell-check).cx.subscribe(&editor, |_host, editor, event: &EditorEvent, cx| { if let EditorEvent::Changed = event { let text = editor.read(cx).text().to_string(); // …save `text`… }}).detach();Rendering
Section titled “Rendering”EditorState is a GPUI entity that renders itself, so a host just renders it as
a child. The editor has no chrome of its own and inherits the ambient text
style (size + color) from its wrapper — set those on the parent:
div() .text_size(px(16.)) .text_color(rgb(0xe6e6e6)) .child(editor.clone())Key bindings
Section titled “Key bindings”bind_keys(cx) binds these in the editor’s "Editor" key context (so they don’t
shadow the host’s shortcuts). cmd-* is macOS; ctrl-* is the cross-platform
equivalent.
| Keys | Action |
|---|---|
typing, backspace, delete, enter | edit text |
← → ↑ ↓, home, end | move caret |
alt-← / alt-→ | word left / right |
shift- + any move | extend selection |
cmd-a | select all |
tab / shift-tab | indent / outdent (list-aware) |
cmd-c / cmd-x / cmd-v | copy / cut / paste |
cmd-z / cmd-shift-z (ctrl-y) | undo / redo |
ctrl-cmd-space | macOS character palette |
escape | dismiss the right-click suggestions menu |
tab/shift-tab indent or outdent the caret’s list item by set_tab_indent
spaces (or insert/remove that many spaces elsewhere), and move between cells when
the caret is in a table.
Events
Section titled “Events”Subscribe with cx.subscribe(&editor, …). EditorEvent:
| Variant | Meaning |
|---|---|
Changed | The text changed via a user edit (typing, delete, paste, IME, applying a suggestion). Not emitted for programmatic set_text. |
OpenLink(SharedString) | A file chip was left-clicked — the host should open the src. The chip stays in the document. |
SelectionChanged | The caret/selection moved without a text change — for updating a caret-anchored affordance (e.g. a table-alignment toolbar). |
API reference
Section titled “API reference”fn bind_keys(cx: &mut App)
Section titled “fn bind_keys(cx: &mut App)”Bind the editor’s editing keys. Call once at startup. Bindings are scoped to the
"Editor" key context.
fn mermaid_sources(content: &str) -> Vec<SharedString>
Section titled “fn mermaid_sources(content: &str) -> Vec<SharedString>”The diagram sources of every ```mermaid block in content, so a host can
pre-render them off-thread before the editor’s mermaid provider is consulted.
struct EditorState
Section titled “struct EditorState”The editor: text + caret/selection state, undo/redo history, and a cached layout
(the wrapped lines from the last paint, for hit-testing + IME). Implements
Render and Focusable.
Construction
Section titled “Construction”fn new(window: &mut Window, cx: &mut Context<Self>) -> Selffn with_text(self, text: impl Into<String>) -> Self // builder; caret at startfn with_placeholder(self, text: impl Into<SharedString>) -> Self // builder; shown when emptyContent
Section titled “Content”fn text(&self) -> &str // borrowedfn value(&self) -> SharedString // ownedfn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>)set_text replaces the whole document, resets the caret to the start, and clears
undo history. It does not emit Changed (it’s a programmatic load).
Caret & geometry
Section titled “Caret & geometry”fn cursor(&self) -> usize // caret byte offsetfn set_cursor(&mut self, offset: usize, cx: &mut Context<Self>) // clamped to a char boundaryfn focus(&self, window: &mut Window, cx: &mut Context<Self>) // enter edit modefn bounds_for_offset(&self, offset: usize) -> Option<Bounds<Pixels>> // window-space caret boxfn last_edit_was_keystroke(&self) -> bool // gate auto-pairing on thisset_cursoronly moves the caret; callfocusto actually receive keyboard input (e.g. when entering edit mode from clicked rendered text).bounds_for_offsetreads the last paint’s layout —Nonebefore the first paint. Use it to anchor a popup (slash menu, toolbar) at a document offset.last_edit_was_keystrokeistrueonly after a single typed character or a single-character backspace — not after a programmatic / multi-char edit (table ops, paste). A host that does its own auto-pairing should gate it on this.
Spell-check / diagnostics
Section titled “Spell-check / diagnostics”fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>, cx: &mut Context<Self>)fn on_suggest(&mut self, provider: impl Fn(&str) -> Vec<String> + 'static)The host computes Diagnostic spans (e.g. with the os-spellcheck crate) and
feeds them in — each underlined with a red squiggle. on_suggest installs the
provider consulted only on right-click of a flagged word (kept lazy because
the OS suggestion call can be slow); it returns replacements, best first, shown
in a popup menu that applies the chosen one on click.
Live Markdown styling (WYSIWYG)
Section titled “Live Markdown styling (WYSIWYG)”fn set_markdown_style(&mut self, style: SyntaxStyle, cx: &mut Context<Self>)fn clear_markdown_style(&mut self, cx: &mut Context<Self>)With a SyntaxStyle installed, the editor renders Markdown live (markers hidden
except around the caret). clear_markdown_style falls back to plain text (spell
squiggles only) — e.g. when the host’s WYSIWYG setting is toggled off.
Block widgets
Section titled “Block widgets”Standalone  lines and ```mermaid blocks render as widgets when the
caret is elsewhere (raw source under the caret). The host owns
loading/caching/rendering and supplies a provider:
fn set_block_image_provider(&mut self, provider: impl Fn(&str) -> Option<Arc<RenderImage>> + 'static)fn set_block_chip_provider(&mut self, provider: impl Fn(&str) -> Option<SharedString> + 'static)fn set_block_mermaid_provider(&mut self, provider: impl Fn(&str) -> Option<Arc<RenderImage>> + 'static)- Image: resolve
src→ a decodedRenderImage(orNonewhile loading → the line shows raw). - Chip: classify an
as a file chip (e.g. a PDF) and return its label → the line renders as a clickable chip; a left-click emitsEditorEvent::OpenLink(src), a right-click places the caret to edit. - Mermaid: resolve a fenced block’s source → a rendered diagram bitmap.
Pre-render with
mermaid_sources.
Indentation
Section titled “Indentation”fn set_tab_indent(&mut self, spaces: usize) // spaces per Tab / list-nesting level (min 1)Table editing
Section titled “Table editing”The editor renders GFM tables as a grid and edits inside the cells. These let a host drive column alignment and structural edits (e.g. from a toolbar or right-click menu); each is a no-op when the caret isn’t in a table.
fn caret_table_align(&self) -> Option<CellAlign> // current column's alignment (header row only)fn set_caret_table_align(&mut self, align: CellAlign, cx: &mut Context<Self>) // rewrites the `|---|` separatorfn insert_table_row(&mut self, below: bool, cx: &mut Context<Self>) // above / below the caret's rowfn delete_table_row(&mut self, cx: &mut Context<Self>) // body rows onlyfn insert_table_column(&mut self, right: bool, cx: &mut Context<Self>) // left / right of the caret's columnfn delete_table_column(&mut self, cx: &mut Context<Self>) // not the last columncaret_table_align returns Some only while the caret is in the header row
(alignment is a per-column property, set once from the header), so it doubles as
“should I show the alignment control?”.
struct SyntaxStyle
Section titled “struct SyntaxStyle”Colors + monospace font for the live-preview styling, supplied by the host so the
editor stays theme-agnostic. All fields are gpui::Hsla except mono: gpui::Font.
| Field | Styles |
|---|---|
marker | dimmed syntax markers (**, `, [, ](…), …) |
code | inline `code` text |
code_bg | inline-code background (also the table row-shade tint) |
link | [text](url), [[wiki-links]], footnote/reference refs |
tag | #tags |
quote | blockquote text + left border (a muted tone) |
mark_bg | <mark> highlight background |
mono | monospace font for inline code + code blocks |
struct Diagnostic
Section titled “struct Diagnostic”pub struct Diagnostic { pub range: Range<usize> } // byte range in the documentA flagged span to underline. &text[range] is the offending word.
enum EditorEvent
Section titled “enum EditorEvent”Changed · OpenLink(SharedString) · SelectionChanged — see Events.
enum CellAlign
Section titled “enum CellAlign”Left · Center · Right — a table column’s text alignment, for
caret_table_align / set_caret_table_align.
A standalone window wired to the real OS spell checker (via the os-spellcheck
crate), live Markdown styling, a PDF-style file chip, and styled tables:
cargo run -p gpui-editor --example demoType to watch the spell squiggles update; right-click a flagged word for suggestions.
Notes & caveats
Section titled “Notes & caveats”- Main thread: like all GPUI UI, drive the editor on the main thread.
- No styling without a
SyntaxStyle: absentset_markdown_style, the editor is a plain-text editor (with spell squiggles if diagnostics are fed in). - The editor caches the last paint’s layout for hit-testing,
bounds_for_offset, and IME — those returnNone/defaults before the first paint.
License
Section titled “License”GPL-3.0-or-later.