This page describes each part of Kaivalya Editor as it is. To build it for the first time, see Getting started; for a task — adding a setting, a command, a benchmark — see the How-to guides; for why it is built this way, see Understanding the architecture.

AI authorship. Most of the source code was written by an AI assistant (Claude), directed, reviewed and decided by the human author. It is said here for transparency: every change was reviewed and validated.

1. The workspace

A Cargo workspace of thirteen crates, Rust edition 2024, licensed AGPL-3.0-or-later. Only kve-ui imports the UI framework; every crate marked headless is tested without a window.

Crate Headless What it owns
kve-core yes The rope buffer, positions in graphemes, edits and undo; colour parsing; the external-formatter runner
kve-config yes Settings and their self-documenting file, the command registry and its keymap, the / palette’s catalogue, sessions, recent places, local history, the two translation catalogues
kve-markdown yes Markdown: HTML for the previews and exports, the outline, the table of contents, tables, lists, links and backlinks, the TODO scan, the task board’s todo.md, the built-in formatter
kve-format yes The other built-in formatters: JavaScript and TypeScript, JSON, CSS, HTML, XML, Vue, YAML, TOML, PHP
kve-fs yes The project tree, the ignore rules, the file watcher, making, renaming and removing files
kve-search yes The file index and its fuzzy match; search and replace across a project
kve-git yes Git through gitoxide: status, diff, staging, commit, log, blame; git itself for fetch, push, rebase and switching branch
kve-canvas yes Obsidian’s JSON Canvas: read, changed and written back field for field; the board’s geometry; its SVG export
kve-ui no The window: every view, pane and dialog, on gpui and gpui-component
kve-preview no The heavy preview: a second program, a window drawing whole HTML pages with Blitz
kaivalya-editor no The executable: arguments, then the window
kve-bench yes The performance harness and the budgets it enforces
kve-drive — A development tool: a pointer and a keyboard for a window under cage

How the crates depend on each other

Arrows point from a crate to what it uses. Everything under kve-ui is headless; kve-ui is the only one that knows a window exists, and nothing depends on it but the executable. kve-bench uses the headless crates directly, to measure them without a window.

flowchart TD bin["kaivalya-editor<br/>the executable"] --> ui["kve-ui<br/>the window"] bin --> config ui --> config["kve-config<br/>settings, commands, translations"] ui --> core["kve-core<br/>buffer, edits, undo"] ui --> md["kve-markdown<br/>markdown, HTML, outline, tables"] ui --> fs["kve-fs<br/>tree, rules, watcher"] ui --> search["kve-search<br/>file index, project search"] ui --> git["kve-git<br/>git through gitoxide"] ui --> format["kve-format<br/>built-in formatters"] ui --> canvas["kve-canvas<br/>JSON Canvas"] ui --> preview["kve-preview<br/>the heavy preview"] config --> md search --> fs ui -.->|spawns, feeds pages over a pipe| previewbin(["kve-preview process"]) bench["kve-bench"] -.-> core & md & fs & search & git & format & canvas & config

The main files

Crate Where to look first
kve-ui window.rs — the editor window: tabs, panes, menus, every command’s handler; canvasview.rs the whiteboard, boardview.rs the task board, logview.rs, gitviews.rs (history, graph, blame), diffview.rs, conflictview.rs, grid.rs the table window, formula.rs, heavy.rs the side of the heavy preview that sends, panewindow.rs a pane in its own window, theme.rs the colour tokens, syntax.rs highlighting, menu.rs the menus
kve-config settings.rs the settings and their annotated file, command.rs the command registry, palette.rs the / palette’s catalogue, session.rs, places.rs, history.rs local history, characters.rs, rules.rs the project file; locales/ the two catalogues
kve-markdown html.rs the HTML of the previews and exports, outline.rs, toc.rs, table.rs, list.rs, board.rs the task board’s todo.md, mentions.rs backlinks, wikilink.rs, transclude.rs embeds, format.rs the formatter, spelling.rs, maths.rs, mermaid.rs, stylesheet.rs
kve-core document.rs the buffer, edit.rs, position.rs graphemes, colour.rs, external.rs running a formatter
kve-fs tree.rs the project tree, rules.rs ignore rules, watch.rs the watcher, make.rs making, renaming, trashing
kve-search index.rs and matcher.rs the file picker, content.rs project search and replace, glob.rs
kve-canvas lib.rs the model, read and written, geometry.rs hit testing and curves, edit.rs the changes, svg.rs the export
kve-git lib.rs: status, diff, commit, log, graph, blame, and the calls out to git
kve-preview main.rs the second program’s window, lib.rs what it is fed

spike/ holds measurements made before a decision, kept outside the workspace so each can pin its own versions. crates/kve-ui/src/window.rs is the editor window — the largest file by far — and each other view has a file of its own: canvasview.rs (the whiteboard), boardview.rs (the task board), logview.rs, gitviews.rs, grid.rs (the table window), formula.rs, heavy.rs (the side of the heavy preview that sends), panewindow.rs (a pane in a window of its own).

2. Toolchain and dependencies

  • Rust 1.98.0, pinned by rust-toolchain.toml with rustfmt and clippy. A bump is deliberate: re-run the benchmarks and record them.
  • Every dependency comes from crates.io, the UI framework included: gpui is the gpui-pre package, gpui-component and its family are 0.6. There are no git dependencies.
  • Cargo.lock is committed and is the pin. Every build and CI job passes --locked. A bare cargo update is never run: it would re-resolve everything to the newest versions and change the framework under a measurement. Removing a crate from the workspace is recorded with cargo update --workspace --offline, which touches only the workspace’s own entries.
  • .cargo/config.toml links with mold through clang when they are installed.

3. Building

make What it does
build The editor with the heavy preview, and kve-preview, in release
build-light The editor alone, without the heavy preview — seconds rather than minutes
check The five checks CI runs, in its order: fmt, clippy, doc, test, bench
fmt cargo fmt --all --check
clippy cargo clippy --workspace --all-targets --locked -- -D warnings
doc cargo doc on every crate, private items included, warnings as errors
test cargo test --workspace --locked
bench kve-bench in release: exits 1 when a budget is missed
site The generated reference pages, then the website with Hugo
walkthrough The checks that need a real window, driven under cage, a screenshot each
deb An Ubuntu/Debian package of the release build, target/deb/kaivalya-editor_<version>_<arch>.deb — tools/deb.sh: make install staged under /usr, binaries stripped, dependencies from dpkg-shlibdeps plus Vulkan and Wayland (opened at run time, not linked). It installs on the Ubuntu it was built on and later
sync Copies project.toml into the files that cannot read it: Cargo.toml’s repository, the metainfo’s developer, home page and repository. A test in kve-config fails until it is run
install / uninstall The two programs, the desktop entry, the AppStream metainfo and the icons, under PREFIX (~/.local by default)

The heavy-preview feature of kaivalya-editor decides whether a build contains the heavy preview’s renderer at all; the heavy_preview setting decides whether a build that has it offers it.

4. The files the editor reads and writes

Where What
~/.config/kaivalya-editor/settings.toml Your settings, written with every option commented when there is none
~/.config/kaivalya-editor/themes/ Your themes, as gpui-component theme JSON
~/.config/kaivalya-editor/styles/ Your stylesheets for the previews and exports
~/.config/kaivalya-editor/templates/ Your templates for new notes
~/.local/share/kaivalya-editor/sessions/<hash>.toml What was open in a project, by an FNV-1a hash of its path
~/.local/share/kaivalya-editor/places.toml Recent and favourite files and folders
~/.local/share/kaivalya-editor/history/ Local history: a version of a file at each save
<project>/.kaivalya/settings.toml A project’s own settings: only what differs from yours
<project>/.kaivalya/project.toml A project’s folder rules, such as the stylesheet a folder’s notes use
<project>/.kaivalya/templates/ A project’s own templates
<project>/todo.md The task board and My tasks

The XDG_CONFIG_HOME and XDG_DATA_HOME variables move the first two folders; the walkthrough and the screenshot script use them to run on a scratch configuration. Nothing is written into a project unless you ask: sessions and history live in your data folder.

5. Settings

  • The defaults are a file, resources/settings/defaults.toml, compiled in. There is no default in Rust: a field of Settings with no value in that file fails the build’s tests.
  • Settings::parse is the only way in. A partial file is merged over the defaults table, so a settings file need only name what it changes. A key the editor no longer knows is stepped over.
  • The file documents itself. Every option is written with its explanation from the catalogue of the reader’s language. A test refuses a setting without one in every catalogue.
  • Tests never read defaults.toml. They run on resources/settings/for-tests.toml through Settings::for_tests() (the test-support feature), whose values do not move; a test holds the two files to the same keys. A test may assert a shape of the real defaults, never a value.
  • A project’s .kaivalya/settings.toml is merged over yours the same way.

6. Commands, menus and keys

  • One registry, crates/kve-config/src/command.rs: each command has an id (file.save), a scope, a catalogue key for its label (command.file.save), a default key binding, and an optional group (a submenu).
  • crates/kve-ui/src/menu.rs maps each id to its gpui action, and bindings() in window.rs installs the keys. The menus, the shortcut reference page and the keymap all read the same table, so a key cannot be shown in one place and bound in another.
  • The text-editing keys (Ctrl+Z, Ctrl+C…) are the editor element’s own, bound in its Input context; a view that is not text — the whiteboard, the log view — handles its keys itself.

7. Translations

  • rust-i18n, two catalogues: crates/kve-config/locales/en-US.yml and fr-FR.yml. Every string a user can see goes through them — t!(), or kve_config::tr() and the helpers beside it for strings with arguments.
  • A test fails when the two catalogues do not have exactly the same keys, or one declares a key twice.
  • The language is the locale setting, or the desktop’s $LANG without one.

8. The heavy preview

kve-preview is a separate program that the editor starts on demand and finds beside itself. The editor feeds it whole HTML pages over a pipe — the document as kve-markdown writes it, with the stylesheet and the formulae already drawn — and it never reads a file or asks the editor anything. Its renderer is Blitz (Stylo, Taffy, Vello), pinned to exact versions. It cannot reach the network.

9. The whiteboard

A .canvas file is read by kve_canvas::parse into a Canvas — nodes, edges, and every field the model does not know, kept to be written back. The board (canvasview.rs) draws that model directly: cards as elements, edges and the grid painted beneath. Every change is written at once, in Obsidian’s layout of one node or edge per line, and only when the text would differ. A write that fails marks the tab unsaved. The board reads its file again when it changes on disk.

10. Git

kve-git does what status, diff, add and commit would, on the repository, with gitoxide — so nothing depends on how the installed git is configured. Where gitoxide has no answer it runs git: fetch, push, rebase, switching and creating a branch.

11. Tests

  • Unit tests in every crate, next to the code.
  • Window tests in kve-ui, with gpui’s test-support: a real window with no compositor. #[gpui::test], a_window_with(…) to open one, VisualTestContext to draw it, and debug_bounds("selector") to find where an element was drawn (selectors are compiled in debug builds only). File dialogs are answered with simulate_path_prompt_response and simulate_new_path_selection. A debounced rebuild needs the clock moved: cx.executor().advance_clock(…), then run_until_parked().
  • A real window: tools/drive.sh start | run | shot | stop runs the editor under cage with no display and drives it with kve-drive — moves, clicks, drags, keys, typing — and grim photographs it. make walkthrough runs the whole list of checks that need one.
  • The definition of done asks for tests of the paths that fail, not only of the ones that work.

12. Performance

  • kve-bench measures each operation a budget is stated for — opening a file, a keystroke’s work, a search, a format, a board’s write — and fails when a 99th percentile is over budget. The budgets come from §4 of the roadmap: 16 ms for anything in a frame, 4 ms for input handling, more for what is asked for once.
  • KVE_TIMINGS=1 makes the editor print a line per step to standard error: each step of opening a project, and the whiteboard’s read, write and frame.
  • The numbers are kept in docs/perf/, one page per phase and one for the feedback passes, with what was measured, on what, and what changed.

13. Continuous integration

.github/workflows/ci.yml installs the system libraries, then runs formatting, clippy, the documentation and the tests; the heavy preview’s own tests and a check of kve-preview with its renderer; and the benchmarks — the commands of make check. The benchmarks that need a real window are run by hand on the reference machine, not in CI.

14. The website

Hugo, in website/, English and French side by side (page.en.md, page.fr.md). The user documentation, in content/docs/, is one page per Diátaxis part, as these developers’ pages are: First steps (tutorial), How-to guides, the User guide (reference — every feature, a section each) and Understanding the editor (explanation); each page’s diataxis: says which, and their weights order them. Three more reference pages are generated from the editor’s own tables by make site — every setting, every shortcut, the / catalogue — so they cannot drift from the code; marked annex: true, they are not listed as pages of their own: the User guide’s Appendices chapter leads to them. chapter_depth: 2 gives the User guide’s card each chapter’s sections on its line. The screenshots come from tools/doc-shots.py, which drives the editor on a demo project, in each language. A page marked draft: true describes the next release and is published with it.

15. Releasing

tools/release.sh check | prepare | finish <version>: check changes nothing; prepare makes every change for a version and leaves the release notes as a skeleton for you to write; finish runs the checks, commits, tags, builds the tarball, the .deb and the site. It never pushes, uploads or deploys — those steps are printed for a person to take.

project.toml, at the root, states once what several files would otherwise each state: the name, the application id, the website, the repository, the author’s site, the developer’s name and the package’s maintainer. The website mounts it as site.Data.project — the footer reads it, and a page writes {{< project repository >}} or {{< project raw "LICENSE.md" >}} rather than an address. release.sh and deb.sh read it through tools/project.sh <key>. Cargo.toml and the metainfo cannot read anything, so make sync copies it into them (prepare runs it), and a test fails when a copy disagrees or when a page writes the repository’s address out again.

16. Features, one by one

Each feature below is described the same way: the path from the key or the menu to what changes, the files and functions involved, how it works, and its tests and measurements. Functions are named as they are in the source; window.rs is crates/kve-ui/src/window.rs, where the editor window — EditorWindow — handles every command.

16.0 The path every command takes

A command is declared once, in kve_config::command::COMMANDS: its id, its scope (File, Project, Editor), its label’s translation key, its default key and its submenu. The menus (menu.rs) and the keymap (window.rs, bindings) are both built from that table, so a command cannot have a key without a menu entry or a label without a translation. Both dispatch a gpui action, which EditorWindow receives in a handler registered with on_action in its render.

flowchart LR reg["kve_config::command<br/>COMMANDS"] --> menu["menu.rs<br/>the menus"] reg --> keys["window.rs<br/>bindings"] menu --> act(["gpui action<br/>e.g. CheckSpelling"]) keys --> act pane["markdown pane row<br/>/ palette row"] --> act act --> handler["EditorWindow handler<br/>e.g. check_spelling"] handler --> pure["pure function<br/>kve-markdown, kve-config…"] pure --> replace["replace_text<br/>one undo step"] replace --> editor["the editor's buffer<br/>and the screen"]

The rule the features follow: the arithmetic — what the document looks like afterwards — is a pure function in a crate with no window (kve-markdown, kve-config, kve-core), tested without a compositor; the handler reads the text and the selection, calls it, and writes the result back with replace_text, which makes the change one undo step. Anything that scales with the document runs on the background executor (cx.background_spawn) and is dropped if the text changed meanwhile.

16.1 Bold, italic, struck through

  • Path: Bold, Italic, Strikethrough → bold / italic / strikethrough → emphasise(Emphasis, …) → kve_markdown::toggle_emphasis → replace_text, then set_selected_range on the words.
  • Files: crates/kve-markdown/src/emphasis.rs, window.rs.
  • How: toggle_emphasis(text, selection, what) returns the new text and the range to select — always the words, never the marks, which is what makes a second press undo the first. Three cases: nothing selected (marks written, caret between them); marks inside the selection (**word** selected whole — taken off); marks just outside it (taken off, or a second press would give ****word****). * being a prefix of ** is guarded, so italic inside a bold word adds rather than strips. A range that is out of bounds or cuts a character returns None rather than panicking.
  • Tests: emphasis::tests — eight, among them pressing_it_twice_puts_the_document_back and italic_inside_a_bold_word_adds_rather_than_removes.

16.2 Lists that continue themselves

  • Path: Enter in the editor’s Input context → enter_in_list (a key handler on the editor element, ahead of the editor’s own newline) → kve_markdown::enter_in_list(text, caret) → replace_text and the caret moved.
  • Files: crates/kve-markdown/src/list.rs, window.rs.
  • How: lines, not a parse — which item the caret is on is a question about one line and the ones above it. enter_in_list returns None outside an item (the editor then breaks the line as usual), the next item for an item with text (same marker, next number with its delimiter, [ ] for a checklist, the rest of the line carried over), the parent’s indentation and marker for an empty nested item, and an empty line for an empty top-level one. Fences above the caret are counted so a line inside a code block is never taken for an item. Shift+Enter is not intercepted.
  • Tests: list::tests (eight); in the window, enter_goes_on_with_a_list_then_up_and_out, shift_enter_in_a_list_only_breaks_the_line.
  • Measured: Enter at the end of a 480 KB document, 1.2 ms p50, 1.5 ms p99, budget 4 ms (kve-bench).

16.3 Formatting a file

  • Path: Format → format → formatter_for(index) decides → markdown: reformat → kve_markdown::format on the main thread; anything else: format_off_thread → kve_format::format (built in) or kve_core::external (a program) on the background executor → the result written back as one edit, the caret kept.
  • Files: crates/kve-markdown/src/format.rs (markdown, dprint plugin plus the subscript guard), crates/kve-format/src/lib.rs (Tier 1: swc via dprint-plugin-typescript, dprint-plugin-json, malva, markup_fmt, pretty_yaml, taplo, mago-formatter), crates/kve-core/src/external.rs (Tiers 2 and 3: rustfmt, gofmt, shfmt, ruff, dprint).
flowchart TD f["formatter_for(document)"] -->|markdown| md["Embedded<br/>kve_markdown::format"] f -->|project has dprint.json| dp["External<br/>dprint"] f -->|built in for the language| bi["Builtin<br/>kve_format::format"] f -->|program installed| ex["External<br/>rustfmt, gofmt, shfmt, ruff"] f -->|none| no["notice: no formatter"]
  • How: every formatter answers Ok(None) when the text is already formatted, so a format that changes nothing makes no edit, no undo entry and no write. A text that does not parse is an error, reported and not written. tab_size, insert_spaces and line_width reach every formatter through FormatOptions / kve_format::Layout. In markdown, a ~subscript~ is hidden from dprint and put back; if it cannot be put back, the format is thrown away (ProtectionLost, F101). format_on_save goes through formats_on_save, the same choice per language.
  • Tests: formatting_lines_up_a_ragged_table, formatting_keeps_the_cursor_where_it_was, format_on_save_writes_the_formatted_text_once, formatting_rust_runs_rustfmt, json_is_formatted_by_the_formatter_built_in, a_language_with_no_formatter_says_so; kve-format’s own per language.
  • Measured: 100 KB of TypeScript built in, 42.7 ms p50; of CSS, 26.4 ms p50 (budget 120 ms, off the main thread); rustfmt on 600 lines through the pipes, 40 ms.

16.4 Checking spelling

  • Path: CheckSpelling → check_spelling (toggles OpenDocument::spelling_on) → schedule_spelling(delay, say) → on the background executor: dictionary_for → prose_words → unknown_words (runs enchant-2) → misspelt → back on the main thread: underline_misspelt → the editor’s decorations.
  • Files: crates/kve-markdown/src/spelling.rs, window.rs.
sequenceDiagram participant U as You participant W as EditorWindow participant B as background executor participant E as enchant-2 U->>W: File › Check spelling W->>B: schedule_spelling(text) B->>B: dictionary_for(front matter lang, else locale) B->>B: prose_words(text) B->>E: each distinct word, one per line E-->>B: the unknown ones B-->>W: misspelt ranges W->>W: text unchanged? underline_misspelt U->>W: types, pauses 120 ms W->>B: schedule_rebuild → schedule_spelling (silent)
  • How: prose_words walks pulldown-cmark’s events and keeps the words of Text outside code blocks, HTML blocks and metadata, then drops those inside a wikilink or the front matter (a sorted list and a binary search), a #tag, and words with a digit or _. unknown_words gives enchant-2 -l -d <dict> each distinct word once, written from another thread so neither pipe fills. A missing program is SpellError::NoChecker, a missing dictionary NoDictionary; either turns the check off and says so. After each pause, schedule_rebuild (the same 120 ms debounce as the preview) calls schedule_spelling silently; a result for a text that has since changed is dropped. The underlines are a TextDecoration collection on the editor, wavy, in the theme’s danger colour.
  • Tests: spelling::tests — only_the_prose_is_offered, the_dictionary_is_the_documents_language, enchant_is_asked_when_there_is_one; in the window, check_spelling_underlines_and_clears, check_spelling_without_the_dictionary_says_so.
  • Measured: the prose words of 480 KB, 11.7 ms p50 (budget 50 ms); enchant-2 on its 4,860 distinct words, 30 ms. KVE_TIMINGS=1 prints a spelling line per check.

16.5 The / palette

  • Path: every keystroke in a markdown document → kve_config::palette::typed(text, caret) (is the word under the caret a /…?) → matching(query) → the menu drawn at the caret → Enter, Tab or a click → choose → replace_text.
  • Files: crates/kve-config/src/palette.rs (the catalogue, matching, what a row writes), crates/kve-config/src/characters.rs (/symbol, /emoji, /entity), templates from resources/templates/ and the project’s; the menu in window.rs (the slash field, slash_act, palette_key).
  • How: typing never commits — typed answers None as soon as the word stops starting with /, so a space, a path or a date never opens it; an empty match is a closed menu; only choose writes. Two levels to browse (sections, then rows), one flat list to search. A row’s body carries one caret marker, where the caret lands. See The / palette for the catalogue.
  • Tests: palette::tests, among them a_path_and_a_date_never_open_it, every_body_marks_one_caret; in the window, a_query_nothing_matches_closes_it_and_keeps_every_character.
  • Measured: the trigger on a 1 MB document, under 0.001 ms; a query with characters (/arr), 0.22 ms p50 (budget 4 ms).

16.6 Tables

  • Path: Insert a table → insert_table → new_table_in → kve_markdown::insert_table(text, at, 3, 2), the caret in the first header cell (the palette’s /table calls the same); Add a row / column, Align → table_row, table_column, align_table → kve_markdown::table_at finds the table under the caret, which is changed and written back aligned; Edit in a grid → table_grid → grid.rs opens a WindowKind::Dialog window → Apply writes back over the remembered range.
  • Files: crates/kve-markdown/src/table.rs, crates/kve-ui/src/grid.rs, window.rs.
  • How: the parsed table is the truth and the grid’s inputs a view of it: every change that is not typing (a row, a column, an alignment, a sort) goes to the table and the inputs are built again; typing goes the other way. The grid is modal, so the byte range remembered at opening is still the table’s at Apply — checked again anyway. Escaped pipes stay inside their cell; a paragraph with pipes is not a table.
  • Tests: table::tests (among them an_escaped_pipe_stays_inside_its_cell); a_table_is_inserted_grown_and_lined_up, a_table_command_with_no_table_under_the_cursor_says_so.

16.7 The formula window

  • Path: Formula → formula → kve_markdown::maths_at(text, caret) finds the $…$ / $$…$$ around the caret (or an empty one at the caret) → formula.rs opens a dialog window with ratex-gpui’s MathEditor → Apply → the TeX written back over exactly that range, in the form it was found in.
  • Files: crates/kve-ui/src/formula.rs, crates/kve-ui/src/maths.rs, crates/kve-markdown/src/maths.rs.
  • How: the document only ever holds TeX; the window holds a tree while it is open. Apply on an untouched formula writes nothing, and a formula whose round trip through the structural editor is not faithful says so before anything is typed. Pressing the key again while it is open closes it. The same RaTeX engine draws the maths in the heavy preview, so what is built is what will be shown.

16.8 Colours

  • Path: ToggleColourPane → toggle_colour_pane → as a window (colour_as = "window", panewindow.rs) or a pane → Insert → insert_colour → write_at_the_cursor(hex_of(colour)), or colour_board when a whiteboard is in front → remember_colour. Separately, on each pause, schedule_swatches → kve_core::colours(text) → a background chip on each #.
  • Files: crates/kve-core/src/colour.rs (finding colours), window.rs (colour_sliders, colour_slider_row, insert_colour, paint_the_colours), crates/kve-ui/src/panewindow.rs.
  • How: a colour is #rgb, #rgba, #rrggbb or #rrggbbaa, found by a byte search for # and only what follows it is read; &#160; is refused (a # after & is a character reference). Only the # is painted, as a background: a foreground would fight the syntax colours, and which one wins is upstream’s hash order (F137). Chips are drawn in CSS, JSON, markdown, XML (so HTML and SVG) and Vue. The window writes where the caret was in the editor window it belongs to; alpha is written only when it is not opaque.
  • Tests: the_colour_pane_writes_its_colour_at_the_cursor, the_colour_key_opens_and_shuts_its_window, a_stylesheet_opens_with_a_chip_on_every_colour, the_colour_window_colours_the_selection_on_a_board; colour::tests.
  • Measured: finding the colours in 100 KB, 0.023 ms p50 (budget 4 ms).

16.9 Characters and emoji

  • Path: ToggleCharactersPane → toggle_characters_pane → open_characters (a window, characters_as) → a click → the character, or its entity, written at the caret → kve_config::characters::remember.
  • Files: crates/kve-config/src/characters.rs (the catalogue and search), crates/kve-ui/src/emoji.rs (the bundled colour emoji font), window.rs.
  • How: one catalogue, two doors — the window and the palette’s /symbol, /emoji, /entity both call characters::search(query, kind, limit). Symbols and entities are one table named in English and French; emoji come from the emojis crate with their CLDR names, English only. The recent list is kept in the session. Emoji draw in colour through the bundled NotoColorEmoji.ttf, added to every window’s font fallbacks.
  • Tests: characters::tests, characters_from_the_palette_and_the_pane, the_characters_as_a_window.
  • Measured: a palette query with characters, 0.22 ms p50.

16.10 Paste from history

  • Path: Ctrl+C / Ctrl+X on their way to the editor → copying → remember_clip; PasteFromHistory → paste_from_history → the list at the caret (clips) → clips_key → take_clip → pasted, and made the newest again.
  • Files: window.rs only.
  • How: a Vec<String> on the window, newest first, without duplicates, cut at clipboard_history; the system clipboard’s current text is put on top when it came from another program. Never written to disk.
  • Tests: paste_from_history_by_the_keys, clipboard_history_zero_keeps_nothing.

16.11 Local history

  • Path: every save → kve_config::history::record (and record_disk before the first save, and when another program changed the file); opening a project → prune_all; LocalHistory → show_local_history → a localhistory.rs tab → a version chosen → the diff view → Restore → restore_selected, through the window.
  • Files: crates/kve-config/src/history.rs, crates/kve-ui/src/localhistory.rs, crates/kve-ui/src/diffview.rs.
  • How: ~/.local/share/kaivalya-editor/history/<hash>/, one folder per file holding the file’s path and one <milliseconds>.snap per version — a folder readable with ls and cat. A version equal to the newest is not kept. Bounded by Keep { count, days }, applied when a file is recorded and to every file when a project opens. Restoring goes through the window, which refuses over unsaved changes.
  • Tests: history::tests (among them the_newest_by_count_and_by_age_are_kept, two_paths_that_share_a_hash_do_not_share_a_folder); the_local_history_keeps_every_save_and_puts_one_back.
  • Measured: keeping a version of 100 KB on save, 4.3 ms p50 (budget 16 ms).

16.12 Pasting a screenshot

  • Path: PasteImage → paste_image → clipboard_image → write_image (into paste_image_folder beside the document, a free name) → a ![](…) link at the caret. Dropped files → drop_files → pictures in markdown are linked through link_dropped (copied beside the document unless already under it); anything else is opened.
  • Files: window.rs.
  • How: three refusals, each with a message: no image on the clipboard, a document never saved (nowhere to be “beside”), a folder that cannot be written. The extension follows the clipboard’s image format.
  • Tests: pasting_with_nothing_on_the_clipboard_says_so, dropping_pictures_links_them_and_opens_the_rest.
  • Path: a keystroke inside [[ or ]( → kve_markdown::complete_path(base, typed, kind) → the list at the caret (paths) → Enter / Tab → a folder goes into it, a file finishes the link.
  • Files: crates/kve-markdown/src/wikilink.rs (complete_path, wikilinks), window.rs.
  • How: what is typed up to its last / names a folder, walked from the document’s own; the rest is the start of a name in it. One read_dir per keystroke, no project index. Folders first; a dotfile only once a dot is typed; eight rows at a time.
  • Tests: a_path_is_written_from_the_folder_the_document_is_in, escape_shuts_the_completion_and_keeps_the_characters, a_rust_file_is_not_offered_completions.
  • Measured: 0.69 ms p99 for a folder of a thousand entries.

16.14 Opening a project

  • Path: Open a folder → open_folder → open_project(root) → the previous project put away (session written, autocommit flushed, search, TODO and git state dropped, the file index dropped) → kve_fs::Tree::open(root, rules) → watch(root, rules) → the session read back → apply_project_settings → the TODO pane and the git pane read again if they are on screen.
  • Files: window.rs (open_project, watch, take_changes, mend_index), crates/kve-fs/src/tree.rs, crates/kve-fs/src/rules.rs, crates/kve-fs/src/watch.rs.
flowchart TD open["open_project(root)"] --> away["put the old one away<br/>session · autocommit · index dropped"] away --> tree["kve_fs::Tree::open<br/>root folder read, nothing deeper"] tree --> watch["watch(root, rules)<br/>registered off the main thread"] tree --> session["session read back<br/>tabs, carets, folds"] session --> settings["apply_project_settings<br/>.kaivalya/settings.toml"] watch --> timer["take_changes, 4 times a second"] timer --> mend["tree rows · mend_index ·<br/>read_again_from_disk"]
  • How: the tree reads a folder the first time it is opened and never before, so a project of 100,000 files costs its root folder. Rules (show_hidden, use_gitignore, .git/ always out) decide what is listed; a change of rules re-judges the names already read — no disk — restarts the watcher and drops the index. The watcher is notify behind a debouncing Pipeline; the window collects what it has settled on with a timer rather than a blocking wait, and applies each change to the tree, to the file index (FileIndex::apply) and to an open document, which is read again (read_again_from_disk, F237) unless it has unsaved changes. A lost event stream throws the index away rather than trusting it. A project that cannot be watched (no inotify watches left) opens anyway.
  • Tests: a_project_that_cannot_be_watched_still_opens, a_lost_event_stream_throws_the_index_away_rather_than_trusting_it, an_ignored_folder_is_watched_while_the_explorer_has_it_open, a_tab_with_unsaved_edits_keeps_them_when_the_disk_changes.
  • Measured: opening a project of 10,000 files, 28.6 ms p50 (budget 60 ms); rebuilding 10,000 explorer rows, 1.4 ms. Registering the watch took 0.6–1.1 s on the main thread until it was moved off it; what the window now waits for when a project opens is about 6 ms.

16.15 The explorer, its filter, and making files

  • Path: a click on a folder → Tree::expand (one read_dir) → rows rebuilt; Show hidden / ignored files → toggle_hidden_files, toggle_ignored_files → Tree::set_rules; the filter box → filter_by → the file index and the fuzzy matcher → refilter → only the folders on the way to a match; the row menu → kve_fs::make::{file, folder, rename, trash}.
  • Files: crates/kve-fs/src/tree.rs, crates/kve-fs/src/make.rs, crates/kve-search/src/index.rs, crates/kve-search/src/matcher.rs, window.rs.
  • How: the filter searches the project’s file index, not the rows on screen, so it finds files in folders never opened; a glob in the box is read as a glob. Making refuses a path that leaves the project (make::inside), never overwrites (write_if_new), and deleting goes to the desktop’s trash (make::trash), which is the undo.
  • Tests: a_filtered_tree_is_the_folders_on_the_way_to_a_match, the_filter_reaches_a_file_the_tree_has_never_read, a_glob_in_the_filter_box_is_a_glob, the_keyboard_walks_the_filtered_rows_rather_than_the_tree, a_path_that_leaves_the_project_is_refused.

16.16 Finding a file by name

  • Path: FindFile → find_file → open_the_picker → build_index if there is none (on the background executor) → each keystroke → Matcher::best(index, query, limit) → Enter → the file opened.
  • Files: crates/kve-search/src/index.rs (FileIndex::build, apply), crates/kve-search/src/matcher.rs (nucleo-matcher’s scoring), window.rs.
  • How: the index is the project’s relative paths under the explorer’s rules, built the first time something asks — the picker, the explorer filter, the wikilinks — and mended from the watcher after that; it goes with the project (F143). A project too large to list says so (is_truncated). The matcher keeps nucleo’s scratch buffers between keystrokes; smart case comes from nucleo’s CaseMatching::Smart.
  • Tests: the_picker_reads_the_project_once_and_finds_by_name, choosing_from_the_picker_opens_the_file_and_shuts_it, the_picker_is_a_toggle_and_leaves_the_keyboard_where_it_found_it.
  • Measured: indexing 10,000 files, 15 ms p50 (budget 200 ms); a fuzzy match over them, 0.31 ms p50 (budget 4 ms).

16.17 Searching and replacing in the project

  • Path: a keystroke in the search box → schedule_search (debounced; the previous search’s abandon flag raised) → on the background executor kve_search::search(root, rules, query, filters, limits, cancel) → Results → the pane. Replace all → a confirmation → replace_all → kve_search::replace with exactly the listed files → open tabs read again → the search run again.
  • Files: crates/kve-search/src/content.rs, crates/kve-search/src/glob.rs, window.rs (schedule_search, replace_all, open_match).
sequenceDiagram participant P as search pane participant W as EditorWindow participant B as background executor P->>W: keystroke W->>W: raise the old search's abandon flag W->>B: after the debounce: search(query, filters) B->>B: walk under the search's rules, memchr per line, skip binary and > 16 MB B-->>W: Results (files, lines, truncated?) P->>W: Replace all, confirmed W->>B: replace(listed files, query, replacement) B->>B: match each file again, write beside, rename over B-->>W: replaced, skipped (unsaved or changed)
  • How: no grep crates — ignore’s walker, memchr for lines, one NUL check for binary. The search’s own rules always leave out what .gitignore excludes unless ignored is ticked; include and exclude globs prune the walk rather than filter results. Results are cut at a thousand files, a hundred matches a file and four hundred characters a line. A replace re-matches each file as it writes it, skips a file open with unsaved changes or changed since, writes to a temporary file and renames it over the original, keeping its permissions.
  • Tests: replacing_changes_what_was_listed_and_says_what_it_did, a_file_with_unsaved_changes_is_left_alone_by_a_replace, a_replace_that_was_cancelled_changes_nothing, the_filter_boxes_narrow_the_search; kve-search’s own.
  • Measured: a content search over 50,000 files, 99 ms p50 (budget 1,000 ms); a project with its build output in it, 14 ms.

16.18 The outline and the table of contents

  • Path: every pause (schedule_rebuild, 120 ms) → markdown: kve_markdown::outline(text); code: symbols::outline(language, text) (a tree-sitter query per language) → the outline pane. Generate or update the table of contents → table_of_contents → kve_markdown::update_table_of_contents → replace_text.
  • Files: crates/kve-markdown/src/outline.rs, crates/kve-markdown/src/toc.rs, crates/kve-ui/src/symbols.rs.
  • How: the outline pane and the table of contents are one reading of the headings, so they cannot disagree. The table of contents lives between <!-- toc --> and <!-- /toc -->: written at the caret the first time, replaced where it is after that, down to toc_max_level. The code outline uses a @definition / @name query per grammar, in the shape of tree-sitter’s tags.scm; a test compiles every query against its grammar.
  • Tests: opening_the_outline_lists_the_headings; outline::tests, toc::tests, symbols::tests.
  • Path: the markdown pane’s Backlinks section opened, a change of document or a save → on the background executor, a walk of the project’s markdown files → kve_markdown::mentions(source, from, target) per file → the section. Generate or update the backlinks → update_mentions → kve_markdown::update_mentions → replace_text.
  • Files: crates/kve-markdown/src/mentions.rs, window.rs.
  • How: a mention is a wikilink (embeds included) or a markdown link or picture, resolved as a click resolves it: a link from the linking file’s folder or the project’s root, a wikilink beside the file, from the root, or by name alone — so [[Notes]] counts for every Notes.md. A path in backticks is not a link. The written block sits between <!-- mentions --> and <!-- /mentions -->, each sentence quoted in a code span so its links are not counted next time.
  • Tests: the_mentions_are_listed_and_written; mentions::tests.
  • Measured: the backlinks of a note over 2,000 markdown files, 204 ms p50 (budget 1,000 ms), off the main thread.

16.20 The TODO pane

  • Path: the pane on screen, or any write in the project → look_for_todos → on the background executor kve_search::search(Query::todos(tags), …) — the project search’s own walk → todo_groups(tags, results) → the pane; todo.md read and parsed by kve_markdown::board into My tasks. A box → tick_item; the arrow → move_to_my_tasks.
  • Files: crates/kve-search/src/content.rs (Query::tags, Query::todos, is_checklist_item), crates/kve-markdown/src/board.rs, crates/kve-markdown/src/inline.rs (a card’s markdown on one line), window.rs (look_for_todos, todo_groups, my_tasks_rows, tick_item, move_to_my_tasks).
  • How: a tag is a whole word as written — TODO: and TODO(ana) yes, todo and MASTODON no — and a line with two tags is listed once, under the first. Checklists are a literal [ ] search, then is_checklist_item keeps only items at the start of a line in markdown. The walk runs only while the pane is on screen, and a newer look abandons the older between files. Ticking re-reads the line first and refuses one that has changed; it is written in the open tab when there is one, as one undo step. Moving a tagged line writes the card into todo.md and takes the comment, or the whole line, out of its file.
  • Tests: the_todo_pane_lists_the_tagged_lines_of_the_project, my_tasks_are_the_projects_todo_file, my_tasks_draws_a_cards_markdown, a_task_is_moved_out_of_the_code_into_my_tasks, a_checklist_item_is_ticked_from_the_pane.
  • Measured: tags over 50,000 files, 105 ms p50 (budget 1,000 ms); a card’s markdown, 1.5 µs a line.

16.21 The task board

  • Path: The task board → show_board → a boardview::BoardView tab that reads the window’s parsed todo.md on every redraw → a drag → move_card → kve_markdown::move_card → written in todo.md’s tab if open, saved at once; + → add_card → kve_markdown::add_card; a subtask’s box → tick_item.
  • Files: crates/kve-markdown/src/board.rs (board, Card, Subtask, move_card, add_card), crates/kve-ui/src/boardview.rs.
  • How: each ## is a column, each top-level item a card; a task indented under a card is its subtask (one level; deeper is the same list; a blank line ends it). A move carries the card’s indented lines. The board holds no copy of the file and reads nothing itself; it follows the open todo.md on every edit. A card’s text goes through gpui-base’s markdown view only when it has a markdown character; subtasks are folded behind their count, because a drawn row costs a frame what a card does.
  • Tests: board::tests (among them tasks_indented_under_a_card_are_its_subtasks), the_board_shows_todo_and_a_dragged_card_moves, a_card_shows_markdown_and_its_subtasks_tick.
  • Measured: moving or adding a card in a todo.md of 500 cards, 0.024 ms; one frame of a 50-card board, every card with three subtasks, 6 ms plain and 12 ms all markdown (3.4 ms before F238).

16.22 Today’s note and templates

  • Path: TodaysNote → todays_note → <daily_note_folder>/<YYYY-MM-DD>.md → if missing, the template daily_note_template filled and written with kve_fs::make::write_if_new → opened.
  • Files: window.rs, crates/kve-config/src/palette.rs (templates as palette rows, the {{…}} markers), resources/templates/ (shipped, copied into the configuration folder only if absent).
  • How: it never overwrites — an existing note is opened. Templates are looked for in the project’s .kaivalya/templates/, then in ~/.config/kaivalya-editor/templates/; a name nobody answers is said, and the note starts empty.

16.23 A project’s own settings, and the project file

  • Path: This project’s settings → open_project_settings → the file opened (made if missing); on opening a project and on saving that file → apply_project_settings(root) → the project’s table merged over yours, over the defaults. Write the project file → write_rule_file → kve_config::rules::rule_template() → .kaivalya/project.toml.
  • Files: window.rs, crates/kve-config/src/settings.rs (Settings::parse, the merge), crates/kve-config/src/rules.rs.
  • How: every option is looked up in the project’s file, then yours, then defaults.toml, so a new option never needs the project file refreshed. An unknown key is named and the rest applies; a file that does not parse leaves your settings untouched. The rule file is generated from the same translation keys as the markdown pane’s help, every key commented out, so writing one into a tree changes nothing until a line is uncommented.

16.24 Sessions, places, and several windows

  • Path: any change worth keeping (a tab, a caret, a fold) → remember → session() → kve_config::session::Session::save_in → one file per project root in the data folder; on opening the project → load_in → surviving() (deleted files dropped). Recent and favourites → kve_config::places. New window → new_window.
  • Files: crates/kve-config/src/session.rs, crates/kve-config/src/places.rs, window.rs.
  • How: nothing is written into the project. Sessions of projects that no longer exist are pruned (prune_in). Each window has its own project; opening a project another window holds brings that window to the front, because two windows would each write the session over the other’s. Quit asks every window in turn and stops at the first Cancel.
  • Tests: the_lists_are_written_down_and_read_back, opening_a_file_puts_it_at_the_front_of_the_recent_list; session::tests, places::tests.

16.25 Reaching a pane without the mouse

  • Path: F6 / Shift+F6 → focus_pane / focus_previous_pane → the next pane an edge is showing, in rail order, then the text → the pane’s own key handler (arrows, Home, End, ← →, Enter).
  • Files: window.rs.
  • How: it stops only at a pane on screen, never at a rail icon. A pane starts on where you are — the outline on the heading above the caret, the explorer and the open files on the file in front. Enter opens and gives the keyboard back to the text.
  • Tests: the_keyboard_goes_round_the_panes_that_are_drawn, the_files_pane_is_walked_from_the_keyboard.

16.26 Git: status, diff and commit

  • Path: any write in the project, or the pane opened → read_git_if_stale → read_git → on the background executor kve_git::open → status, branch, upstream, remotes → the pane (git_pane). A double-click → show_diff → kve_git::diff / hunks / side_by_side → a diffview.rs tab. Commit → commit_git → kve_git::commit(repo, paths, message).
  • Files: crates/kve-git/src/lib.rs, crates/kve-ui/src/diffview.rs, window.rs (git_pane, read_git, commit_git, show_diff).
flowchart LR pane["Git pane"] -->|read| gix["kve-git<br/>gitoxide in process"] gix --> st["status · diff · commit<br/>log · graph · blame<br/>conflicts · resolve"] pane -->|network, branches| git["kve-git<br/>runs your git"] git --> net["fetch · push · rebase<br/>switch · create_branch"]
  • How: everything that reads or commits goes through gitoxide, so the installed git and its configuration do not matter; nothing in kve-git is fast enough for a frame, so the window always calls it off the main thread. commit stages the ticked paths (a deleted one is removed), takes the author from git’s configuration, and refuses an empty message or a commit that would change nothing. The diff is against HEAD with three lines of context; close changes share a hunk, and a replacement is set beside what it replaced.
  • Tests: kve-git’s tests.rs — among them only_the_files_chosen_are_committed, a_commit_that_cannot_be_made_says_why, the_diff_is_against_head_with_context; in the window, the_git_pane_lists_and_commits_what_is_ticked, enter_in_the_message_commits.
  • Measured: git status of this repository, 2.3 ms p50 (budget 120 ms).

16.27 Git: branches and the network

  • Path: the branch name → branch_menu → a branch → switch_branch (unsaved documents asked about first) → kve_git::switch → open documents read again or closed. New branch… → start_naming_a_branch → create_named_branch → kve_git::create_branch. Fetch, Push, Rebase (drawn by git_actions) → run_network(Network::…) → kve_git::fetch, push, rebase → the documents rewritten are read again (follow_the_rebase).
  • Files: crates/kve-git/src/lib.rs, window.rs.
  • How: these five run the installed git, so ssh keys, credential helpers and proxies work as in a terminal; prompts are turned off (GIT_TERMINAL_PROMPT=0 and friends), so a remote that wants a password fails with git’s sentence instead of hanging. switch refuses to overwrite a change and names the files; push sets an upstream the first time; rebase refuses while anything else is in progress, and a conflict runs git rebase --abort — there is no Continue button behind it. A running command is drawn in the accent colour, and a second press does nothing.
  • Tests: switching_writes_the_branch_and_back, switching_refuses_to_overwrite_and_carries_the_rest, push_and_fetch_through_git, a_rebase_that_conflicts_is_undone, a_rebase_refuses_when_it_should; in the window, switching_branch_rewrites_the_open_document, fetch_and_push_from_the_pane, rebase_from_the_pane.

16.28 Git: history, graph, blame and conflicts

  • Path: History → show_history → kve_git::log(repo, 500) → a gitviews::HistoryView tab → a commit → kve_git::details → a file → open_commit_diff → kve_git::diff_in_commit. Graph → show_graph → kve_git::graph → gitviews. Blame this file → blame_file → kve_git::blame. A U file double-clicked → conflictview.rs → kve_git::conflicts → Take ours / theirs / both → kve_git::resolve → Mark as resolved → kve_git::mark_resolved.
  • Files: crates/kve-git/src/lib.rs, crates/kve-ui/src/gitviews.rs, crates/kve-ui/src/conflictview.rs.
  • How: the graph sorts commits topologically, children first even when two share a second, and gives each line of descent a lane and a colour. Blame describes the last commit’s version, and draws only the rows on screen (F207), as the log view does. A conflict block is read from the markers in the file, which stays the truth: a block fixed by hand simply leaves the merge tab. An empty message on a merge commit uses merge_message, the one git prepared.
  • Tests: the_log_is_newest_first_and_blame_names_each_line, the_graph_draws_a_branch_and_its_merge, the_topological_sort_puts_children_first, conflict_markers_are_read_and_resolved, a_conflicted_merge_is_resolved_and_committed; in the window, history_and_blame_open_windows.
  • Measured: the log, 500 commits, 20 ms p50; the graph of every branch, 21 ms; the blame of a 22,000-line file, 220 ms (budget 1,000 ms), all off the main thread.

16.29 Committing on its own

  • Path: a save → saved_for_autocommit (the rule read by autocommit_rule from .kaivalya/project.toml; asked once by ask_about_autocommit) → a timer, restarted on each save (autocommit_wait) → take_autocommit → on the background executor kve_git::autocommit(repo, paths) then kve_git::push → commit_and_push. Leaving the project, closing or quitting → autocommit_now.
  • Files: crates/kve-git/src/lib.rs (autocommit), window.rs, crates/kve-config/src/session.rs (AutocommitAnswer: the answer and the rule it was given to).
  • How: autocommit takes only the saved paths that differ from HEAD, their names as the message, and returns None while a merge, a rebase or a cherry-pick is in progress; a conflicted file is never taken. The answer to the question is stored with the rule it answered, so a changed rule asks again. On closing the window the commit and the push run there and then, and the window waits for them; on opening another project they run off the main thread.
  • Tests: autocommit_takes_the_saved_files_that_changed; an_autocommit_pushes_and_leaving_the_project_does_not_wait, closing_the_window_commits_and_pushes_what_was_waiting.
  • Measured: commit and push on closing, one file, a bare remote on the same disk, 24–31 ms (budget 100 ms); a remote across a network adds its own round trip.

16.30 The live preview

  • Path: TogglePreview / ToggleSplit → toggle_preview / toggle_split → the document’s mode (edit, preview, split) → on each pause schedule_rebuild → the source, its soft breaks joined (kve_markdown::join_soft_breaks), given to the preview’s TextViewState::markdown → gpui-base’s markdown view. In split view, a caret that changed section → preview_follows_the_caret.
  • Files: window.rs, crates/kve-markdown/src/preview.rs, crates/kve-markdown/src/outline.rs.
  • How: the live preview is the toolkit’s own markdown view, so it costs nothing extra and repaints at every pause; it draws in the editor’s theme and shows what it does not know ($…$, [!TIP], mermaid) as written. The caret, the scroll and a click on a heading are kept in step through the outline’s mapping of headings to blocks, the same one a click in the outline scrolls by. Nothing is built for a preview that is not on screen.
  • Tests: nothing_is_built_for_a_preview_that_is_not_open, the_preview_follows_the_caret_from_heading_to_heading.

16.31 The heavy preview

  • Path: OpenHeavyPreview → open_heavy_preview → heavy::Preview spawns kve-preview → on each pause schedule_heavy_preview → heavy_page → kve_markdown::page(source, PageOptions) (HTML, the chosen stylesheet inlined, maths and diagrams drawn as SVG) → written to the process’s standard input → kve-preview draws the latest page with Blitz.
flowchart LR subgraph editor["kaivalya-editor"] src["the document"] --> page["kve_markdown::page<br/>to_html · stylesheet · embeds"] page --> svg["maths: ratex → SVG<br/>mermaid: mermaid-rs-renderer → SVG"] svg --> pipe["heavy::Preview<br/>one page per frame on stdin"] end pipe --> preview["kve-preview<br/>Blitz: html5ever · Stylo · Vello"] preview --> win(["its own window"])
  • Files: crates/kve-ui/src/heavy.rs (what is sent, and the process), crates/kve-preview/src/main.rs (the renderer), crates/kve-markdown/src/page.rs, crates/kve-markdown/src/html.rs, crates/kve-ui/src/maths.rs.
  • How: the preview is fed a page; it never reads a file or asks the editor anything, so making it a pane later is a change of transport. Two gates: the heavy-preview cargo feature decides whether the build has a renderer and a diagram engine at all, heavy_preview whether a build that has them offers them. A pipe that refuses a page means the process has gone: the handle is dropped and the status bar says so. The page is the export’s, but for one <base> line that lets the window find your pictures on disk.
  • Tests: the_heavy_preview_draws_markdown_and_html_and_nothing_else, the_heavy_preview_says_it_is_off_rather_than_doing_nothing; page::tests.
  • Measured: the page for 100 KB of markdown, 17 ms p50 (budget 120 ms); twenty formulae as SVG, 6 ms; a six-node mermaid flowchart, 56 ms (551 ms the first time). The preview process holds about 117 MB while it is open.

16.32 Which stylesheet a document is drawn with

  • Path: a change of document, of front matter or of a rule → refresh_markdown_rules → kve_markdown::stylesheet_for(document, source, chosen, stop) → the footer’s chip (stylesheet_chip) and the page. The chip → switch_stylesheet → the choice kept in the session.
  • Files: crates/kve-markdown/src/stylesheet.rs (stylesheet_for, folder_rule), crates/kve-ui/src/styles.rs (the styles folder, the bundled sheets written out), window.rs.
  • How: four answers in order — your choice for this document, the front matter’s css:, the longest matching [[css]] rule in .kaivalya/project.toml (the walk upwards stops at the project’s root), the bundled GitHub sheet. A named file that is not there is still the answer, so a typo is reported rather than hidden. The answers are cached: the footer reads them every frame, and no syscall belongs in a paint path. The front matter scan is bounded; the part that touches files runs only when that scan changes.
  • Tests: stylesheet::tests — choosing_the_bundled_one_beats_the_front_matter, the_longest_path_wins_and_the_projects_styles_come_first; in the window, the_switcher_offers_the_stylesheets_in_the_project, only_a_rendered_file_has_a_stylesheet_to_argue_about.

16.33 Exporting as HTML

  • Path: Export as HTML… → export_html → the dialog (pictures, browser drawing, back buttons, stylesheet) → export_html_page → heavy_page_for_export(scripts, back_button) → kve_markdown::with_images (pictures as data URIs, or copied beside) → written. Export the project… → export_project_asked → a folder → export_project_into → the same per markdown file, links between pages rewritten to .html.
  • Files: window.rs, crates/kve-markdown/src/export.rs, crates/kve-markdown/src/page.rs.
  • How: the export is the heavy preview’s page minus its <base> line. Drawn here, the page names no script at all; left to the browser, it keeps the TeX and the mermaid source and names MathJax and mermaid.js by export_mathjax_url / export_mermaid_url, quoted so an address cannot break out of its attribute. Two pictures of one name do not become one. The project export walks under git’s rules and leaves out the folder it writes into; a link to a file that is not a page is left as written and counted.
  • Tests: the_export_asks_before_it_writes, an_export_is_one_file_with_its_pictures_inside_it, an_export_can_put_its_pictures_beside_it_instead, the_second_export_hands_the_maths_and_the_diagram_to_the_browser, a_project_is_exported_page_by_page, an_export_takes_the_stylesheet_chosen_for_it; export::tests, page::tests.

16.34 How markdown is read

  • Path: every reading of a document — the preview, the page, the outline, spelling — goes through kve_markdown::reading::options(), the one set of pulldown-cmark options; to_html_with(source, HtmlOptions) makes the HTML.
  • Files: crates/kve-markdown/src/reading.rs, html.rs, callout.rs (alerts), wikilink.rs, transclude.rs (embeds, with a guard against a note that embeds itself), maths.rs, mermaid.rs, tag.rs, front_matter.rs.
  • How: one reading, so the outline, the table of contents, the preview and the export cannot disagree about what a document holds. Headings carry the anchors the outline hands out, two identical headings getting two distinct ones. Footnotes are gathered wherever they are written. Maths is found by maths() with the rules of the reference document; a fence is never maths.
  • Tests: html::tests (among them headings_carry_the_anchors_the_outline_handed_out), callout::tests, transclude::tests, maths::tests; the reference document’s tests in crates/kve-markdown/tests/reference_document.rs.
  • Measured: 100 KB of markdown to HTML, 19 ms p50 (budget 120 ms); finding the maths in it, 0.85 ms.

16.35 The log view

  • Path: a .log file opened, or View as log → view_as_log → a logview.rs tab → kve_core::log::LineIndex::of(path) (where each line starts) → the rows on screen read from the file as they are drawn. A filter, a level or a time range → one pass over the file on the background executor → the lines kept. Go to time… → kve_core::log::first_at. A timer once a second → LineIndex::extend → what was appended is indexed and filtered on its own.
  • Files: crates/kve-core/src/log.rs (level_of, stamp_end, moment_of, bound, first_at, LineIndex), crates/kve-ui/src/logview.rs.
  • How: the file is never loaded — eight bytes a line for the index, a small cache of the rows drawn. A level is a word near the start of a line in any of its usual spellings; a line without one (a stack trace) takes the previous line’s. A time is read as a number that sorts (yyyymmddhhmmssmmm), so a range is the filter’s own pass, and Go to time halves the index, reading a few lines at each of about twenty steps, which is why it assumes the file is in time order. A file that got shorter is read again from the start.
  • Tests: a_log_is_filtered_followed_and_opened_as_text, a_log_is_searched_by_dates; log::tests.
  • Measured: indexing a 50 MB log of 500,000 lines, 7.6 ms p50 (budget 500 ms); filtering it for a word, 158 ms (budget 1,000 ms, off the main thread); going to a time, 0.65 ms.

16.36 Whiteboards

  • Path: a .canvas opened → kve_canvas::parse → a canvasview.rs tab drawing the Canvas → each gesture decided from the board’s geometry (kve_canvas::geometry) → the model changed (kve_canvas::edit) → kve_canvas::to_json written at once when it differs. Export as SVG… → kve_canvas::svg::to_svg. New whiteboard → new_whiteboard; Show as whiteboard → show_whiteboard.
  • Files: crates/kve-canvas/src/lib.rs (the model, read and written), geometry.rs (hit testing, sides, curves), edit.rs, svg.rs; crates/kve-ui/src/canvasview.rs.
flowchart LR file[".canvas file"] -->|parse| model["Canvas<br/>nodes · edges · unknown fields kept"] model --> view["canvasview.rs<br/>cards as elements, edges painted"] view -->|gesture| geo["geometry<br/>what is under the pointer"] geo --> edit["edit<br/>move · resize · connect · group"] edit --> model model -->|to_json, if changed| file model -->|to_svg| svg["picture.svg"]
  • How: the model keeps every field it does not know and writes it back unchanged, in Obsidian’s layout of one node or edge per line, so a change to a card is a change to one line in git. Cards off screen are not built; below a size where a card’s words would be a few pixels, it draws its frame alone, and it offers its connectors and resize handle only when at least 48 × 32 pixels on screen. A write that fails keeps the change on the board and marks the tab unsaved. The board reads its file again when it changes on disk.
  • Tests: a_canvas_opens_on_the_board_and_what_is_dropped_is_written, a_window_started_on_a_canvas_shows_the_board, the_board_exports_as_one_svg, the_text_colour_setting_reaches_the_board_and_its_picture; kve-canvas’s own.
  • Measured: writing a 2,000-card canvas, 2.4 ms p50 (budget 16 ms); reading it, 4.6 ms; what a press is on, 0.2 ms; the SVG export, 23 ms. KVE_TIMINGS=1 prints the board’s read, writes and frames.

16.37 The panes, the rail and pane windows

  • Path: a click on a rail icon → reveal(pane) or the edge folded; a hover → peek(pane) (when peek_on_hover); a pane shown as a window (colour_as, characters_as) → toggle_pane_window → panewindow.rs.
  • Files: window.rs (Pane, reveal, peek, side_of, open_on), crates/kve-ui/src/panewindow.rs, crates/kve-ui/src/icon.rs.
  • How: each edge shows one pane; which pane goes on which edge is its <pane>_side setting, and the icons are in rail_order (one list for both edges; unknown names stepped over, repeats counted once, unlisted panes after). A peek draws the pane over the text and occludes what is beneath; a drag that starts in it keeps it open until the release. A pane window draws the editor window’s own pane contents — no second copy of the state — and hands what it writes back to the window that owns the document.
  • Tests: the_rail_keeps_every_icon_and_pushes_the_open_one_in, the_rail_follows_the_order_it_is_given, hovering_an_icon_peeks_the_pane_over_the_editor, a_rail_told_not_to_peek_does_not, a_drag_out_of_a_peek_keeps_it_open_until_the_release, an_edge_folds_to_its_rail_and_comes_back.

16.38 Languages and highlighting

  • Path: a file opened → its language from its name → the toolkit’s tree-sitter grammar → captures mapped to theme tokens by syntax.rs. The footer’s language, or Language… → choose_language_action → choose_language → the colours, the outline, the markdown pane and the formatter follow.
  • Files: crates/kve-ui/src/syntax.rs, crates/kve-ui/src/code.rs, window.rs.
  • How: nothing looks inside a file to guess; the name decides, and a choice in the footer corrects it for this file while it is open (a Save as drops it). A capture a theme does not name is drawn in the ordinary foreground, so a gap looks like a grammar that does not highlight. HTML is highlighted with the XML grammar: the bundled HTML grammar does not parse HTML (F119).
  • Tests: a_language_chosen_overrides_the_name; syntax::tests, code::tests.

16.39 Themes and fonts

  • Path: at start and on a change of settings → theme::install(setting) → apply_configured ("light", "dark", or apply_named) → set_fonts → settled: the interface and code families checked against what is installed (available_fonts, installed, has_bold). Every colour the editor draws comes from theme::tokens(cx).
  • Files: crates/kve-ui/src/theme.rs, resources/themes/*.json (written into ~/.config/kaivalya-editor/themes/ at start), crates/kve-ui/src/emoji.rs.
  • How: a theme is a JSON file in the toolkit’s format with a schema beside it. A family that is not installed is swapped here, before the framework does it, so the window’s fallback chain (the colour emoji font) survives (F171); the swap takes the first fallback with a bold of its own, because a variable font is drawn at one weight (F242). A family named in the settings is kept as it is; one nothing answers to is said in the status bar.
  • Tests: theme::tests — among them a_missing_family_becomes_the_one_the_framework_would_draw, a_fallback_with_a_bold_is_taken_first; every_theme_round_trips_through_the_file.
  • Measured: at most twenty font lookups, once, at start.

16.40 The settings file

  • Path: Ctrl+, → open_settings → the annotated file written in your language if missing, refreshed with any new option and your values kept (the old one as settings.toml.bak) → opened. At start → Settings::parse(your file) merged over resources/settings/defaults.toml. Reset → reset_settings, after asking.
  • Files: crates/kve-config/src/settings.rs, resources/settings/defaults.toml, resources/settings/for-tests.toml, crates/kve-config/locales/*.yml (each option’s comment, settings.*).
  • How: Settings::parse is the only door: a partial file is merged over the defaults table, so an option added later never needs your file refreshed. An unknown key is reported and the rest applies; a broken file is an error to read but never stops the editor. Tests run on for-tests.toml, whose values do not move, and never assert what defaults.toml says (F139). See How-to guides for adding an option.
  • Tests: every_setting_is_explained_in_every_shipped_locale, the_two_settings_files_name_the_same_keys, a_partial_file_fills_the_rest_from_the_defaults, an_unknown_key_is_reported_rather_than_silently_ignored, a_file_written_before_an_option_existed_gains_it_and_keeps_what_it_had, the_settings_example_in_the_readme_actually_parses; in the window, the_settings_command_writes_the_annotated_file_and_opens_it, resetting_the_settings_asks_first_and_keeps_the_old_file.