vim-core-rs is a Rust-facing host integration layer around one embedded
upstream Vim runtime. It is written for LLM-first consumption, with humans as
the second audience. The goal of this README is to remove ambiguity before an
agent reads source code, edits behavior, or integrates the crate into a host
application.
This crate is not a generic editor toolkit. It exists to give another application access to Vim's modal editing engine, buffer state, selected rendering data, host-mediated virtual document I/O, and host-managed job bridging.
Read these documents in order when you need the full mental model.
README.mdThis page. It explains purpose, invariants, boundaries, and the most dangerous assumptions to avoid.docs/SCOPE.mdThe formal in-scope and out-of-scope boundary for the crate.docs/known-limitations.mdCurrent implementation gaps, intentionally incomplete areas, and features that exist in the type surface but are not fully implemented.docs/api-index.mdThe map to the exhaustive public and internal API references.docs/api-contracts.mdThe behavior contracts that matter more than raw signatures.docs/public-api-reference.mdThe full crate-public symbol reference.docs/internal-api-reference.mdThe implementation-only helper and coordination reference.
The crate owns the embedded Vim editing core. The host application owns the environment around it.
The crate does these jobs:
- Creates exactly one embedded Vim runtime per process.
- Executes Normal-mode and Ex-mode commands against that runtime.
- Extracts coherent snapshots of text, cursor, buffers, windows, undo state, search data, syntax chunks, and pop-up menu state.
- Converts file-like commands into host-visible VFS requests instead of letting embedded Vim perform direct file I/O.
- Bridges Vim job and channel behavior into host-managed processes through virtual file descriptors.
The crate does not do these jobs:
- It does not own the UI event loop or rendering pipeline.
- It does not persist files by itself.
- It does not spawn or supervise real OS processes by itself.
- It does not aim to expose the whole Vim runtime as a general-purpose scripting platform.
These points are not optional design preferences. They are repository-level truths enforced by code and tests.
- Only one live
VimCoreSessionmay exist per process. VimCoreSessionis stateful, notSend, and notSync.take_pending_host_action()is part of the normal control flow, not an optional diagnostics API.- VFS requests are explicit and host-owned. The host must answer them with
submit_vfs_response(). - Job execution is host-owned. The host must react to
JobStart, then feed bytes back withinject_vfd_data()and lifecycle updates withnotify_job_status(). - Contract tests are the source of truth for behavior when prose and intuition disagree.
The safest way to reason about the crate is as four coupled state machines.
- Session machine Owns lifetime, the embedded runtime pointer, and the single-session lock.
- Command machine Executes Normal or Ex input and returns one coarse outcome instead of a full diff.
- VFS machine Tracks buffer-to-document bindings, request sequencing, request ledger status, and deferred close behavior.
- VFD machine Tracks virtual file descriptors and host-managed job status.
Do not model the crate as "a library that edits files." That framing leads to incorrect assumptions about ownership, persistence, and concurrency.
An embedding host must implement these behaviors.
- Prefer
execute_normal_command()andexecute_ex_command()when you need one transaction result that contains the final snapshot, emitted events, and emitted host actions. - Drain
take_pending_host_action()until it returnsNone. - Drain
take_pending_event()until it returnsNonewhen you consume asynchronous or queue-delivered editor events outside a transaction result. - Handle
CoreHostAction::WriteandCoreHostAction::Quit. - Handle every
CoreHostAction::VfsRequestand send one matchingCoreVfsResponse. - Spawn real jobs when
CoreHostAction::JobStartappears. - Handle
CoreHostAction::JobWrite { vfd, data }by forwarding the Vim-originated bytes to the real process stdin. - Feed stdout and stderr bytes back through
inject_vfd_data(). - Report terminal job status through
notify_job_status(). - Set UI size with
set_screen_size()when screen geometry matters. - Treat
window_idas the only canonical pane identity. Resolve focus withactive_window_id()or the unique active entry inCoreSnapshot.windows, not with host-side ordering or buffer heuristics. - Read
CoreEvent::Message,CoreEvent::PagerPrompt,CoreEvent::Bell,CoreEvent::Redraw, and layout or buffer lifecycle events from the transaction result or event queue. Do not scrape:messagesorv:errmsg.
An agent reading this repository must not assume any of the following.
- Do not assume file writes happen automatically inside the crate.
- Do not assume
CoreCommandOutcome::HostActionQueuedmeans the side effect already completed. - Do not assume every Ex command reaches native Vim unchanged. A file-like subset is intercepted and rerouted through Rust policy.
- Do not assume VFS request IDs are reusable or unordered. They are monotonic session-local identities.
- Do not assume the latest
CoreVfsResponse::Savedalways applies. Save responses can be rejected as stale when revisions advance. - Do not assume the embedded runtime owns process execution. It only requests host action.
- Do not assume pane identity or active-pane resolution from window ordering,
buffer identity, or host layout heuristics. Use
window_id,active_window_id(), and per-window snapshot fields.
When repository artifacts disagree, use this order.
- Contract tests in
tests/ - Rust implementation in
src/ - Native bridge implementation in
native/ - Documentation in
docs/ - README-style summaries elsewhere
This hierarchy is intentional. The crate is contract-driven, and many observable behaviors are locked by tests rather than by Rust type signatures.
These paths matter most for reasoning and maintenance.
src/lib.rsPublic API facade, command routing, snapshot conversion, event and host action queues, option accessors, and top-level VFS integration.src/vfs.rsVFS request ledger, transaction log, buffer bindings, and deferred close.src/vfd.rsVirtual file descriptor and job-bridge state.native/C bridge and embedded Vim runtime shims.tests/Contract suites that define large parts of the intended behavior.build.rsandbuild_*.rsBindgen, native compilation, audit generation, and traceability artifacts.
The vendored Vim build is intentionally constrained.
- The generated upstream build runs with
--with-features=normal. - The build disables native terminal support.
- The build disables native socket server support.
- The build disables native channel support at configure time, even though the crate exposes host-bridged job behavior through its own control plane.
Because of that setup, do not assume this repository behaves like a full desktop Vim or like Neovim.
By default, consumers do not build the embedded Vim runtime from source.
build.rs resolves a target-specific prebuilt artifact, expands it into
OUT_DIR, and links the bundled libvimcore.a. A Git checkout of this
repository is the exception: when VIM_CORE_FROM_SOURCE is unset, local
repository builds use the source build path so tests exercise the checked-out
native and vendored Vim sources.
The published crate still contains the allowlisted vendor/vim_src inputs
required for an explicit source build. That means a crates.io consumer can
override the default path with VIM_CORE_FROM_SOURCE=1 without checking out
this repository.
This repository currently publishes prebuilt artifacts for these targets:
aarch64-apple-darwinx86_64-unknown-linux-gnu
Each target artifact contains these files:
libvimcore.abindings.rsartifact-manifest.json
The archive also includes the generated traceability outputs that the
repository tests expect in OUT_DIR, such as the compile proof, audit
reports, generated Vim headers, and generated upstream test list.
Use these environment variables to control the build path:
VIM_CORE_FROM_SOURCE=1Builds the embedded Vim runtime from source. This is the expected mode for repository development, CI, and release packaging.VIM_CORE_ARTIFACT_BASE_URLOverrides the base URL or local directory used to resolve the prebuilt archive. This is intended for validation, mirrors, or local testing.VIM_CORE_ARTIFACT_DIROverrides the local cache directory used to store downloaded and verified prebuilt artifacts.
If a prebuilt artifact is unavailable, the build fails with an explicit error.
Consumer builds do not fall back to a source build unless you set
VIM_CORE_FROM_SOURCE=1.
Repository development uses the source build. This section explains which GitHub Actions workflow to use as you move from a local code change to a published crate release.
Run the baseline checks from the repository root with these commands:
./scripts/setup-git-hooks.sh
VIM_CORE_FROM_SOURCE=1 cargo test
VIM_CORE_FROM_SOURCE=1 cargo publish --dry-run --allow-dirtyIn a Git checkout, bare cargo test also uses the source build path unless you
explicitly set VIM_CORE_FROM_SOURCE=0.
After you run ./scripts/setup-git-hooks.sh once, the repository installs a
pre-commit hook through core.hooksPath. That hook runs rustfmt on staged
.rs files before each commit, then re-stages those formatted Rust files so
the commit includes the formatted result.
When you validate the default consumer path before publication, point
VIM_CORE_ARTIFACT_BASE_URL at a local directory that contains the packaged
target archives, then run a clean build with VIM_CORE_FROM_SOURCE=0.
Use the workflows according to the scope of your change. The fast path is for day-to-day Rust and bridge work. The slow paths are for upstream Vim coverage and publication.
CIThis workflow runs onpush,pull_request, and manual dispatch. It checks formatting, runs clippy, and runs the repository contract tests withcargo test -- --skip upstream_. Use it for normal development and PR validation.Upstream Vim TestsThis workflow runs only by manual dispatch. It runscargo test --test upstream_vim_generatedand covers the vendored upstream Vim test cases that are too expensive for every push. A successful local build or a successfulCIrun does not replace this workflow because those checks do not exercise the full vendored upstream Vim script suite. Use it when the change could affect embedded Vim behavior relative to upstream Vim.Release CrateThis workflow runs only by manual dispatch. It updates the vendored Vim snapshot, verifies source builds, packages prebuilt artifacts, verifies the default consumer path, and optionally publishes release assets and the crate.
Upstream Vim Tests is not required for every code change. Use this rule:
- Skip it for documentation-only changes, workflow-only changes, and Rust-only changes that do not affect vendored Vim behavior.
- Run it after changing
vendor/vim_src,vendor/upstream/vim,native/,build.rs, or command execution paths that could change Vim script behavior, runtime discovery, message handling, job handling, or other embedded Vim behavior.
In short, if you can confidently explain that the change does not alter
embedded Vim behavior, CI is enough. If the change could alter behavior
relative to upstream Vim, run Upstream Vim Tests before release.
Use this sequence when you change code and want to carry it through to a published release.
- Change the code locally and run
VIM_CORE_FROM_SOURCE=1 cargo test. - Push the branch and wait for
CIto pass. Treat this as the required baseline for ordinary Rust, VFS, VFD, and bridge-facing changes. - If the change touches
vendor/vim_src,vendor/upstream/vim,native/,build.rs, or behavior that depends on upstream Vim scripts, runUpstream Vim Testsmanually from the Actions tab and wait for it to pass. Otherwise, you can skip that workflow. - Before preparing a release, run
VIM_CORE_FROM_SOURCE=1 cargo publish --dry-run --allow-dirtylocally if you need an immediate packaging sanity check. - When you are ready to cut a release, start
Release Cratemanually and set these inputs:vim_version: the upstream Vim tag to vendor, such asv9.2.0437crate_version: the crate version to publish, such as0.2.0publish:falsefor a full dry run without publishing, ortrueto continue through the approval and publish stages
- Wait for
Release Crateto finish its non-publishing stages:update_upstream,verify_source,package_prebuilt,verify_prebuilt, andpackage_check. - If you started
Release Cratewithpublish=false, review the produced results and rerun the workflow withpublish=truewhen you are ready to release the same version. - If you started
Release Cratewithpublish=true, approve thecrates-io-publishenvironment when GitHub prompts for manual approval. - Let
Release Cratepublish the GitHub release assets first, then publish the crate to crates.io. This ordering is required because the default consumer build expects the GitHub Releases artifact to exist before users install the crate version.
For a production release, publish GitHub Releases assets for every supported
target before you publish the crate to crates.io. That ordering keeps
vim-core-rs = "<version>" consumers from hitting a transient 404 between the
crate publish and the asset upload.
This repository vendors and modifies portions of upstream Vim in order to embed one headless Vim runtime behind the Rust API surface. When you redistribute this repository or binaries built from it, include the bundled license files that apply to the shipped code.
The repository uses a split licensing model:
- Original
vim-core-rscode is licensed under Apache License 2.0. SeeLICENSE. - Vendored and modified Vim sources remain subject to the Vim License. See
LICENSE-vim.
The upstream Vim baseline for the current vendor snapshot is recorded in
upstream-metadata.json. The repository-level notice for this modified Vim
distribution lives in THIRD_PARTY_NOTICES.md, including the maintainer
contact URL for change requests related to the embedded Vim sources.
Read docs/SCOPE.md to understand design boundaries. Read
docs/known-limitations.md before planning changes so you do not mistake a
current gap for a guaranteed feature.