Developer Hub
Quickstart
A focused guide for the quickstart stage of extension development. coc.nvim extensions expose an activate(context) entry point to the Node.js extension host.
Extension quickstart
The recommended path is to let an Agent Skills-compatible coding agent scaffold and verify the project with coc-create. A manual setup is included below when you need full control.
Create the project with coc-create
The coc-create skill generates a TypeScript extension with a sample command, strict type checking, an esbuild production build, coc-test, and CI coverage for both Vim and Neovim.
1. Check the requirements
- Node.js 22.15 or newer
- Vim and Neovim for the two editor test lanes
- Codex or another Agent Skills-compatible coding agent
2. Install coc-create for Codex
| 1 | npx skills@latest add neoclide/coc-skills \ |
| 2 | --skill coc-create \ |
| 3 | --agent codex \ |
| 4 | --global \ |
| 5 | --yes |
Start a new Codex task after installation so the agent can discover the newly installed skill.
3. Describe the extension to the agent
| 1 | $coc-create |
| 2 | |
| 3 | Create a new coc.nvim extension in ~/vim-dev/coc-word-count. |
| 4 | |
| 5 | Package name: coc-word-count |
| 6 | Description: Show the word count of the current buffer |
| 7 | Initial command: word-count.show |
| 8 | Behavior: Display the word count of the current buffer |
| 9 | License: MIT |
| 10 | Author: Your Name |
| 11 | |
| 12 | Install the dependencies and verify the extension with type checking, |
| 13 | a production build, and coc-test using both Neovim and Vim. |
What gets verified
Type checking, the production build, both coc-test editor lanes, and npm pack --dry-run.
Explicit project boundaries
Use an empty target directory. Git initialization, commits, pushes, and npm publication remain explicit developer decisions.
Manual setup
Create the package files yourself when an AI-assisted scaffold is not appropriate.
Create an empty package
Install the SDK
Export activate()
Build and test
| 1 | mkdir coc-demo && cd coc-demo |
| 2 | npm init -y |
| 3 | npm install --save-dev coc.nvim@next coc-test 'typescript@^6' 'esbuild@^0.28' '@types/node@^22' |
| 4 | mkdir -p src test |
| 5 | # Copy the package.json, tsconfig.json, esbuild.mjs, src/index.ts, and |
| 6 | # test/extension.test.ts shown below, then run: |
| 7 | npm run typecheck |
| 8 | npm run build |
| 9 | npm run test:nvim |
| 10 | npm run test:vim |
| 1 | { |
| 2 | "name": "coc-demo", |
| 3 | "version": "0.1.0", |
| 4 | "description": "A coc.nvim extension example", |
| 5 | "main": "lib/index.js", |
| 6 | "files": ["lib"], |
| 7 | "engines": { "node": ">=22.15.0", "coc": "^0.0.82" }, |
| 8 | "activationEvents": ["onCommand:coc-demo.hello"], |
| 9 | "contributes": { "commands": [{ "command": "coc-demo.hello", "title": "Say hello" }] }, |
| 10 | "scripts": { |
| 11 | "build": "node esbuild.mjs", |
| 12 | "prepare": "npm run build", |
| 13 | "typecheck": "tsc -p tsconfig.json --noEmit", |
| 14 | "test": "npm run test:nvim", |
| 15 | "test:nvim": "coc-test --nvim "test/**/*.test.ts"", |
| 16 | "test:vim": "coc-test --vim "test/**/*.test.ts"" |
| 17 | }, |
| 18 | "coc-test": { "entryFile": "src/index.ts" } |
| 19 | } |
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "target": "ES2022", |
| 4 | "module": "ES2022", |
| 5 | "moduleResolution": "Bundler", |
| 6 | "lib": ["ES2022"], |
| 7 | "strict": true, |
| 8 | "noEmit": true, |
| 9 | "allowImportingTsExtensions": true, |
| 10 | "esModuleInterop": true, |
| 11 | "skipLibCheck": true, |
| 12 | "forceConsistentCasingInFileNames": true |
| 13 | }, |
| 14 | "include": ["src/**/*.ts", "test/**/*.ts"] |
| 15 | } |
| 1 | import { build } from 'esbuild' |
| 2 | |
| 3 | await build({ |
| 4 | entryPoints: ['src/index.ts'], |
| 5 | bundle: true, |
| 6 | external: ['coc.nvim'], |
| 7 | format: 'cjs', |
| 8 | platform: 'node', |
| 9 | target: 'node22', |
| 10 | outfile: 'lib/index.js', |
| 11 | }) |
| 1 | import { commands, ExtensionContext, window } from 'coc.nvim' |
| 2 | |
| 3 | export const commandId = 'coc-demo.hello' |
| 4 | |
| 5 | export function activate(context: ExtensionContext): void { |
| 6 | context.subscriptions.push(commands.registerCommand(commandId, async (name?: string) => { |
| 7 | const message = `Hello from ${name ?? 'coc.nvim'}!` |
| 8 | await window.showInformationMessage(message) |
| 9 | return message |
| 10 | }) |
| 11 | } |
| 1 | import assert from 'node:assert/strict' |
| 2 | import { beforeEach, describe, it } from 'node:test' |
| 3 | import { commands, workspace } from 'coc.nvim' |
| 4 | import { commandId } from '../src/index.ts' |
| 5 | |
| 6 | beforeEach(async () => { |
| 7 | await workspace.nvim.command('enew!') |
| 8 | }) |
| 9 | |
| 10 | describe('coc-demo', () => { |
| 11 | it('activates and registers its command', () => { |
| 12 | assert.equal(commands.has(commandId), true) |
| 13 | }) |
| 14 | |
| 15 | it('executes through the editor-backed coc.nvim runtime', async () => { |
| 16 | const message = await commands.executeCommand(commandId, 'Vim') |
| 17 | assert.equal(message, 'Hello from Vim!') |
| 18 | assert.equal(await workspace.nvim.eval('bufexists(bufnr())'), 1) |
| 19 | }) |
| 20 | }) |
| 1 | # From the extension project after npm run build |
| 2 | # Use either editor; the runtimepath method is supported by coc.nvim. |
| 3 | vim -c 'set runtimepath^=/absolute/path/to/coc-demo' -c 'edit demo.txt' |
| 4 | # Or: nvim -c 'set runtimepath^=/absolute/path/to/coc-demo' -c 'edit demo.txt' |
| 5 | |
| 6 | # In the opened Vim/Neovim session, invoke the contributed command: |
| 7 | :CocCommand coc-demo.hello |
runtimepath: install dependencies, build lib/index.js, then prepend the project root. Use :CocInstall only for a published npm or GitHub package; the runtimepath method keeps local iteration separate from an npm release.Continue with the typings reference
Use the generated project as a base, then find the current commands, workspace, language, window, and extension APIs for the feature you want to build.