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.
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.tomlwithrustfmtandclippy. A bump is deliberate: re-run the benchmarks and record them. - Every dependency comes from crates.io, the UI framework included:
gpuiis thegpui-prepackage,gpui-componentand its family are 0.6. There are no git dependencies. Cargo.lockis committed and is the pin. Every build and CI job passes--locked. A barecargo updateis 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 withcargo update --workspace --offline, which touches only the workspace’s own entries..cargo/config.tomllinks withmoldthroughclangwhen 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 ofSettingswith no value in that file fails the build’s tests. Settings::parseis 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 onresources/settings/for-tests.tomlthroughSettings::for_tests()(thetest-supportfeature), 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.tomlis merged over yours the same way.
6. Commands, menus and keys
- One registry,
crates/kve-config/src/command.rs: each command has anid(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.rsmaps each id to itsgpuiaction, andbindings()inwindow.rsinstalls 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 itsInputcontext; 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.ymlandfr-FR.yml. Every string a user can see goes through them —t!(), orkve_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
localesetting, or the desktop’s$LANGwithout 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, withgpui’stest-support: a real window with no compositor.#[gpui::test],a_window_with(…)to open one,VisualTestContextto draw it, anddebug_bounds("selector")to find where an element was drawn (selectors are compiled in debug builds only). File dialogs are answered withsimulate_path_prompt_responseandsimulate_new_path_selection. A debounced rebuild needs the clock moved:cx.executor().advance_clock(…), thenrun_until_parked(). - A real window:
tools/drive.sh start | run | shot | stopruns the editor undercagewith no display and drives it withkve-drive— moves, clicks, drags, keys, typing — andgrimphotographs it.make walkthroughruns 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-benchmeasures 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=1makes 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.
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, thenset_selected_rangeon 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 returnsNonerather than panicking. - Tests:
emphasis::tests— eight, among thempressing_it_twice_puts_the_document_backanditalic_inside_a_bold_word_adds_rather_than_removes.
16.2 Lists that continue themselves
- Path:
Enterin the editor’sInputcontext →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_textand 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_listreturnsNoneoutside 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+Enteris 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:
Enterat 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::formaton the main thread; anything else:format_off_thread→kve_format::format(built in) orkve_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,dprintplugin plus the subscript guard),crates/kve-format/src/lib.rs(Tier 1: swc viadprint-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).
- 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_spacesandline_widthreach every formatter throughFormatOptions/kve_format::Layout. In markdown, a~subscript~is hidden fromdprintand put back; if it cannot be put back, the format is thrown away (ProtectionLost, F101).format_on_savegoes throughformats_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);
rustfmton 600 lines through the pipes, 40 ms.
16.4 Checking spelling
- Path:
CheckSpelling→check_spelling(togglesOpenDocument::spelling_on) →schedule_spelling(delay, say)→ on the background executor:dictionary_for→prose_words→unknown_words(runsenchant-2) →misspelt→ back on the main thread:underline_misspelt→ the editor’s decorations. - Files:
crates/kve-markdown/src/spelling.rs,window.rs.
- How:
prose_wordswalks pulldown-cmark’s events and keeps the words ofTextoutside 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_wordsgivesenchant-2 -l -d <dict>each distinct word once, written from another thread so neither pipe fills. A missing program isSpellError::NoChecker, a missing dictionaryNoDictionary; either turns the check off and says so. After each pause,schedule_rebuild(the same 120 ms debounce as the preview) callsschedule_spellingsilently; a result for a text that has since changed is dropped. The underlines are aTextDecorationcollection on the editor, wavy, in the theme’sdangercolour. - 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-2on its 4,860 distinct words, 30 ms.KVE_TIMINGS=1prints aspellingline 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,Tabor 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 fromresources/templates/and the project’s; the menu inwindow.rs(theslashfield,slash_act,palette_key). - How: typing never commits —
typedanswersNoneas 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; onlychoosewrites. 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 thema_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/tablecalls the same); Add a row / column, Align →table_row,table_column,align_table→kve_markdown::table_atfinds the table under the caret, which is changed and written back aligned; Edit in a grid →table_grid→grid.rsopens aWindowKind::Dialogwindow → 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 theman_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.rsopens a dialog window withratex-gpui’sMathEditor→ 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)), orcolour_boardwhen 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,#rrggbbor#rrggbbaa, found by a byte search for#and only what follows it is read; 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 andsearch),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,/entityboth callcharacters::search(query, kind, limit). Symbols and entities are one table named in English and French; emoji come from theemojiscrate with their CLDR names, English only. The recent list is kept in the session. Emoji draw in colour through the bundledNotoColorEmoji.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+Xon 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.rsonly. - How: a
Vec<String>on the window, newest first, without duplicates, cut atclipboard_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(andrecord_diskbefore the first save, and when another program changed the file); opening a project →prune_all;LocalHistory→show_local_history→ alocalhistory.rstab → 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>.snapper version — a folder readable withlsandcat. A version equal to the newest is not kept. Bounded byKeep { 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 themthe_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(intopaste_image_folderbeside the document, a free name) → alink at the caret. Dropped files →drop_files→ pictures in markdown are linked throughlink_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.
16.13 Links to other files
- 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. Oneread_dirper 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.
- 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 isnotifybehind a debouncingPipeline; 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(oneread_dir) →rowsrebuilt; 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_indexif 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’sCaseMatching::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 executorkve_search::search(root, rules, query, filters, limits, cancel)→Results→ the pane. Replace all → a confirmation →replace_all→kve_search::replacewith 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).
- How: no
grepcrates —ignore’s walker,memchrfor lines, oneNULcheck for binary. The search’s own rules always leave out what.gitignoreexcludes 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 totoc_max_level. The code outline uses a@definition/@namequery per grammar, in the shape of tree-sitter’stags.scm; a test compiles every query against its grammar. - Tests:
opening_the_outline_lists_the_headings;outline::tests,toc::tests,symbols::tests.
16.19 Backlinks
- 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 everyNotes.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 executorkve_search::search(Query::todos(tags), …)— the project search’s own walk →todo_groups(tags, results)→ the pane;todo.mdread and parsed bykve_markdown::boardinto 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:andTODO(ana)yes,todoandMASTODONno — and a line with two tags is listed once, under the first. Checklists are a literal[ ]search, thenis_checklist_itemkeeps 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 intotodo.mdand 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→ aboardview::BoardViewtab that reads the window’s parsedtodo.mdon every redraw → a drag →move_card→kve_markdown::move_card→ written intodo.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 opentodo.mdon every edit. A card’s text goes throughgpui-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 themtasks_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.mdof 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 templatedaily_note_templatefilled and written withkve_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.
Enteropens 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 executorkve_git::open→status,branch,upstream,remotes→ the pane (git_pane). A double-click →show_diff→kve_git::diff/hunks/side_by_side→ adiffview.rstab. 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).
- How: everything that reads or commits goes through gitoxide, so the
installed
gitand its configuration do not matter; nothing inkve-gitis fast enough for a frame, so the window always calls it off the main thread.commitstages 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 againstHEADwith three lines of context; close changes share a hunk, and a replacement is set beside what it replaced. - Tests:
kve-git’stests.rs— among themonly_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 statusof 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 bygit_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=0and friends), so a remote that wants a password fails with git’s sentence instead of hanging.switchrefuses to overwrite a change and names the files;pushsets an upstream the first time;rebaserefuses while anything else is in progress, and a conflict runsgit 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)→ agitviews::HistoryViewtab → 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 byautocommit_rulefrom.kaivalya/project.toml; asked once byask_about_autocommit) → a timer, restarted on each save (autocommit_wait) →take_autocommit→ on the background executorkve_git::autocommit(repo, paths)thenkve_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:
autocommittakes only the saved paths that differ fromHEAD, their names as the message, and returnsNonewhile 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 pauseschedule_rebuild→ the source, its soft breaks joined (kve_markdown::join_soft_breaks), given to the preview’sTextViewState::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::Previewspawnskve-preview→ on each pauseschedule_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-previewdraws the latest page with Blitz.
- 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-previewcargo feature decides whether the build has a renderer and a diagram engine at all,heavy_previewwhether 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 byexport_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 themheadings_carry_the_anchors_the_outline_handed_out),callout::tests,transclude::tests,maths::tests; the reference document’s tests incrates/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
.logfile opened, or View as log →view_as_log→ alogview.rstab →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
.canvasopened →kve_canvas::parse→ acanvasview.rstab drawing theCanvas→ each gesture decided from the board’s geometry (kve_canvas::geometry) → the model changed (kve_canvas::edit) →kve_canvas::to_jsonwritten 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.
- 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=1prints 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)(whenpeek_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>_sidesetting, and the icons are inrail_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", orapply_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 fromtheme::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 thema_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 assettings.toml.bak) → opened. At start →Settings::parse(your file)merged overresources/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::parseis 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 onfor-tests.toml, whose values do not move, and never assert whatdefaults.tomlsays (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.