A minimal Neovim plugin to send the current buffer to a running opencode CLI.
Using lazy.nvim:
{
"yourusername/nvim-opencode",
opts = {
-- Optional configuration
keymaps = {
send_buffer = "<leader>ob",
prompt_command = "<leader>oc",
insert_context = "<leader>oi",
toggle_prompt = "<leader>ot",
clear_prompt = "<leader>ol",
},
default_keymaps = true, -- Set to false to disable default keymaps
port_cache_ttl = 60, -- Cache port discovery for 60 seconds
}
}Or with packer.nvim:
use "yourusername/nvim-opencode"<leader>ob- Send current buffer to opencode<leader>oc- Prompt for a command to send to opencode<leader>oi- Insert context placeholder into PROMPT buffer<leader>ot- Toggle PROMPT buffer visibility<leader>ol- Clear PROMPT buffer content
PROMPT Buffer Features:
- Type
@in INSERT mode to trigger file autocomplete - Fuzzy search through project files
- Automatically includes referenced files in context when sent to opencode
The plugin provides the following user commands:
:Opencode- Send current buffer to opencode:OpencodeCommand- Prompt for command and send to opencode:OpencodeContext- Add context to PROMPT buffer:OpencodePrompt- Toggle PROMPT buffer:OpencodeClear- Clear PROMPT buffer
Backward compatibility aliases (deprecated, use the commands above):
:OpencodeSendBuffer→:Opencode:OpencodePromptCommand→:OpencodeCommand:OpencodeInsertContext→:OpencodeContext:OpencodeTogglePrompt→:OpencodePrompt
You can customize the plugin by calling the setup function:
require('nvim-opencode').setup({
-- Customize keymaps
keymaps = {
send_buffer = "<leader>os", -- Custom keymap for sending buffer
prompt_command = "<leader>op", -- Custom keymap for prompt command
insert_context = "<leader>oi", -- Custom keymap for inserting context
toggle_prompt = "<leader>ot", -- Custom keymap for toggling prompt
clear_prompt = "<leader>ol", -- Custom keymap for clearing prompt
},
-- Disable default keymaps (useful if you want to define your own)
default_keymaps = false,
-- Cache port discovery for N seconds (reduces latency after first use)
port_cache_ttl = 120, -- Cache for 2 minutes
})You can register custom context placeholders:
require('nvim-opencode').register_context(
"@mycontext",
function(context)
-- Your custom logic here
return "custom content"
end,
"Description of your custom context"
)Context Registration Validation: The plugin validates context placeholders at registration time. Placeholders must:
- Match the
@wordformat (e.g.,@mycontext) - Have a handler function that receives a Context object and returns a string
You can integrate the opencode connection status into your statusline:
-- For lualine
require('lualine').setup({
sections = {
lualine_x = { require('nvim-opencode').status }
}
})
-- For other statuslines
vim.o.statusline = vim.o.statusline .. '%{luaeval("require(\'nvim-opencode\').status()")}'The status function returns a short status string based on server.get_cached_port(), for example:
[opencode :8080]- Connected (shows port number)[opencode disconnected]- Disconnected or not cached
Set the OPENCODE_PORT environment variable to specify the port if auto-detection doesn't work:
export OPENCODE_PORT=8000- Neovim
- curl
- lsof (for auto-detecting the opencode server)
Optional (for better performance):
- ripgrep (
rg) - For fast file discovery in PROMPT buffer@file autocomplete- Install from: https://github.com/BurntSushi/ripgrep
- Falls back to pure-Lua file discovery if not available
nvim-opencode follows a layered architecture for maintainability and extensibility:
Top-level flow
plugin/nvim-opencode.lua– defines user commands and default keymaps and calls into the public module.lua/nvim-opencode/init.lua– public API surface that re-exports high-level operations.lua/nvim-opencode/client.lua– orchestrates all user-facing flows (send buffer, prompt command, PROMPT buffer, context insertion).
Layers
- HTTP Layer (
http.lua): Handles all HTTP communication with opencode server - API Layer (
api/*.lua): Business logic for messages and commands - UI Layer (
ui/*.lua): User interface components (command selector, context picker, file selector, PROMPT buffer) - Context Layer (
context.lua): Context construction and interpolation - Server Layer (
server.lua): Server discovery and port resolution with caching - Config Layer (
config.lua): Configuration management and context registry accessors - Utilities (
utils.lua,notify.lua,prompt_buffer.lua): Shared helpers, file discovery, and PROMPT buffer management with@trigger
This design ensures:
- Single responsibility per module
- Easy testing through clear boundaries
- Simple extension points for new features
The context system lets you embed location-aware placeholders in your prompts and expand them into rich strings before sending to opencode.
Core pieces:
context.lua– Defines theContextobject, which captures buffer, window, cursor position, visual selection, diagnostics, quickfix list, and git diff.config.lua– Registers built-in placeholders, validates custom ones, and exposes helpers likeget_contexts()/get_context_descriptions().ui/context_picker.lua– UI picker that lets you choose a context placeholder to append into the PROMPT buffer.ui/file_context_selector.lua– File autocomplete UI for@trigger in PROMPT buffer.prompt_buffer.lua– Manages thePROMPTbuffer where interpolated context content and user input live, with@trigger for file references.utils.lua– File discovery (using ripgrep or pure-Lua) and fuzzy filtering for file autocomplete.
Built-in placeholders:
| Placeholder | Description | Example Output |
|---|---|---|
@this |
Visual selection or cursor position | @file.lua L10:C5 or @file.lua L10:C5-L15:C20 |
@buffer |
Current buffer path | @myfile.lua |
@buffers |
All open buffers | @file1.lua @file2.lua @file3.lua |
@diagnostics |
Current buffer diagnostics | 2 diagnostics in @file.lua\n- L21:C10 (lua_ls): error |
@quickfix |
Quickfix list entries | @file.lua L10:C5 @file.lua L20:C10 |
@diff |
Git diff output | Raw git diff content |
For a deeper dive into placeholder formats, buffer-aware behavior, and interpolation details, see:
docs/architecture/context-interpolation.mddocs/architecture/buffer-aware-contexts.mddocs/features/context-system.md
You can register your own placeholders using:
require('nvim-opencode').register_context('@mycontext', function(ctx)
-- ctx is a Context instance
return 'custom content based on ctx'
end, 'My custom context')These placeholders are expanded by Context:interpolate(text), which scans the text for @placeholder patterns (with optional @file and range markers) and replaces them using the appropriate handler.
For a deeper dive into the overall architecture and context system internals, see docs/architecture/architecture.md.
nvim-opencode automatically subscribes to opencode's server-sent event (SSE) stream and forwards all events as Neovim User autocmds. This enables you to react to opencode activity programmatically.
All opencode events are triggered as User autocmds with the pattern OpencodeEvent. You can listen to them using standard Neovim autocommand patterns:
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeEvent",
callback = function(args)
local event = args.data.event -- The event object from opencode
local port = args.data.port -- The port of the opencode server
-- React to specific event types
if event.type == "session.idle" then
vim.notify("opencode finished responding")
end
end,
})The autocmd data contains:
event: The decoded JSON event object from opencodetype: Event type string (e.g., "session.idle", "file.edited")properties: Table with event-specific properties (optional)
port: The port number of the opencode server that emitted the event
Based on opencode's event stream, common event types include:
server.connected- opencode server startedsession.idle- opencode finished processing and is waitingsession.error- An error occurred in the sessionmessage.updated- opencode is updating a message (streaming response)message.part.updated- A message part was updatedfile.edited- opencode edited a file (properties include file path)permission.updated- opencode requesting permissionpermission.replied- Permission request was answered
Display status notifications:
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeEvent",
callback = function(args)
local event = args.data.event
if event.type == "session.idle" then
vim.notify("opencode is ready", vim.log.levels.INFO)
elseif event.type == "session.error" then
vim.notify("opencode error occurred", vim.log.levels.ERROR)
end
end,
})Auto-reload edited files:
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeEvent",
callback = function(args)
local event = args.data.event
if event.type == "file.edited" and event.properties then
local filepath = event.properties.path
-- Reload the buffer if it's open
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) then
local bufname = vim.api.nvim_buf_get_name(buf)
if bufname == filepath then
vim.api.nvim_buf_call(buf, function()
vim.cmd("checktime")
end)
break
end
end
end
end
end,
})Filter specific events:
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeEvent",
callback = function(args)
local event = args.data.event
-- Only react to file edits
if event.type == "file.edited" then
print(string.format("opencode edited: %s", event.properties.path))
end
end,
})Connection Management:
The SSE connection is automatically managed:
- Connects when opencode port is discovered
- Automatically reconnects if opencode restarts on a different port
- Handles connection errors gracefully
- No user intervention required
For running tests, install plenary.nvim as a dependency:
Using lazy.nvim:
{
"yourusername/nvim-opencode",
dependencies = {
"nvim-lua/plenary.nvim", -- for testing
},
}Run tests with: nvim --headless -c "lua require('plenary.test_harness').test_directory('spec')"