Contributing to neocursor.nvim

July 31, 2026 · View on GitHub

Issues and pull requests are welcome. This is a beta chasing 1:1 parity with Cursor's Tab, so bug reports about behavior that differs from Cursor are especially useful.


Reporting a bug

Two commands make almost every report actionable — please include their output:

:NeocursorDebug

Resolved config, sidecar state, last error, and whether the current buffer is eligible. Redact nothing but paths you consider private — it prints no token.

uv run cursor_paths.py   # in the plugin directory

Required for anything path- or auth-related. It prints every candidate location checked and which one won, which usually identifies the problem outright.

Also helpful: your OS, how Cursor was installed (official build, Flatpak, Snap, AUR, WSL-side), Neovim version (nvim --version), and :NeocursorLog output if the failure is intermittent.


Project layout

lua/neocursor/
  init.lua        state machine, <Tab> handling, sidecar protocol, context collection
  preview.lua     ghost text + diff rendering (extmarks, virt_text / virt_lines)
  heuristics.lua  suppression rules pulled live from Cursor's CppConfig

sidecar.py        stdio bridge: Neovim JSON ⇄ Cursor StreamCpp (Connect/protobuf over h2)
cursor_paths.py   platform path resolution — also runnable standalone as a diagnostic

docs/             installation variants, full config reference, troubleshooting
demo/             the recording harness that generates assets/demo.gif
test/             specs; all of these run in CI on macOS, Linux and Windows
poc/              protocol spikes kept for reference; never loaded at runtime

The README is the storefront — pitch, install, usage, and a summary of each option. Anything longer than a screen belongs in docs/, linked from the matching README section. test/docs_links.py enforces that those links keep resolving.

The Lua side never talks to the network — it speaks line-delimited JSON to the sidecar over stdio, and the sidecar owns everything about Cursor's protocol. Keep that boundary intact.


Running the tests

Requires uv, Python 3.12+, and Neovim on PATH. No test framework to install: the specs are plain scripts that exit non-zero on failure.

# every relative link and anchor across README + docs/ resolves
python test/docs_links.py

# path resolution across all three platform layouts (synthesized, no Cursor needed)
python test/paths_spec.py

# sidecar startup contract against a synthesized Cursor install
python test/handshake_spec.py

# the tab-tab-tab flow: canned sidecar, real jobstart plumbing, live insert session
nvim --headless -u NONE -c "luafile test/flow_spec.lua"

# hint chrome: rendering + show_hints normalization
nvim --headless -u NONE -c "luafile test/hints_spec.lua"

# the same behavioral suite with hint chrome disabled — chrome must never
# change behavior, so these assertions must pass identically
NEOCURSOR_SPEC_NO_HINTS=1 nvim --headless -u NONE -c "luafile test/flow_spec.lua"

# the demo harness, headless: pilots a real Neovim through jump → accept →
# jump → accept and fails if any beat lands wrong (needs tmux)
DEMO_ATTACH=0 ./demo/drive.sh

None of them require you to be signed into Cursor; they synthesize an install (test/fake_cursor_home.py) and use a canned sidecar (test/fake_sidecar.py). The live backend is deliberately out of scope for CI — it's httpx over TLS, not something a runner can meaningfully exercise.

What CI runs

Every push and PR runs the full suite on ubuntu-latest, windows-latest and macos-latest. Platform matters here more than in most plugins: neocursor reads Cursor's session off disk, and that path moves per OS.

The demo harness also runs headlessly on Linux. It is there so that a change to the tab flow breaks the build rather than quietly leaving a README GIF that shows behavior the plugin no longer has.


Regenerating the demo GIF

assets/demo.gif is a build artifact, not a hand-recorded file. Rebuild it with:

./demo/record.sh          # needs vhs, tmux, neovim, python3

The recording uses a canned backend, so it works without a Cursor subscription and produces essentially the same output every run — which is what makes it reviewable in a pull request instead of an opaque binary.

Change what happens on screen in demo/drive.sh (keystrokes and pacing) or demo/scenario.py (the code being edited). Do not change the terminal geometry casually: the tape's 906x520 is chosen to yield exactly the 84x20 pane drive.sh pins tmux to, and anything else reflows the code. demo/README.md explains the design and the traps in more detail.


Pull requests

  • One concern per PR. A rendering fix and a protocol change are two PRs.
  • Add a spec. If a bug could regress silently, it needs an assertion. New options should cover both the default and the non-default path.
  • Keep the default path unchanged unless that is the change, and say so loudly in the description if it is.
  • Match the surrounding style. The codebase comments the why — especially where behavior mirrors a specific Cursor mechanism — and skips narrating the what. Follow the file you're editing.

Commit messages

Conventional prefixes, lowercase subject, em-dash for elaboration:

feat: show_hints — opt out of the hint chrome
fix: stop the ghost surviving a buffer switch
docs: add vim.pack install instructions
test: set USERPROFILE alongside HOME in the path spec
chore: bump the CI matrix to Neovim stable

Reference the issue in the body (Closes #4) rather than the subject line.


Scope

neocursor is an interoperability layer, not a model host. Changes that add a second backend, bundle credentials, or redistribute Cursor source are out of scope — see Legal. Anything that brings the plugin closer to Cursor's actual Tab behavior is in scope by default.