Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Install the plugin with your favorite package manager. See the [Configuration](#
```lua
-- Default configuration with all available options
require('opencode').setup({
preferred_picker = nil, -- 'telescope', 'fzf', 'mini.pick', 'snacks', 'select', if nil, it will use the best available picker. Note mini.pick does not support multiple selections
preferred_picker = nil, -- 'telescope'/'telescope.nvim', 'fzf'/'fzf-lua', 'mini.pick', 'snacks'/'snacks.nvim', 'select', if nil, it will use the best available picker. Note mini.pick does not support multiple selections
preferred_completion = nil, -- 'blink', 'nvim-cmp','vim_complete' if nil, it will use the best available completion
default_global_keymaps = true, -- If false, disables all default global keymaps
default_mode = 'build', -- 'build' or 'plan' or any custom configured. @see [OpenCode Agents](https://opencode.ai/docs/modes/)
Expand Down
11 changes: 9 additions & 2 deletions lua/opencode/api_client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,16 @@ end
--- List messages for a session
--- @param id string Session ID (required)
--- @param directory string|nil Directory path
--- @param opts? { limit?: number } Optional query parameters
--- @return Promise<OpencodeMessage[]>
function OpencodeApiClient:list_messages(id, directory)
return self:_call('/session/' .. id .. '/message', 'GET', nil, { directory = directory })
function OpencodeApiClient:list_messages(id, directory, opts)
local query = { directory = directory }
if opts then
for k, v in pairs(opts) do
query[k] = v
end
end
return self:_call('/session/' .. id .. '/message', 'GET', nil, query)
end

--- Create and send a new message to a session
Expand Down
5 changes: 3 additions & 2 deletions lua/opencode/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,14 @@ end)

---Get messages for a session
---@param session Session
---@param opts? { limit?: number } Optional query parameters (e.g. limit)
---@return Promise<OpencodeMessage[]>
function M.get_messages(session)
function M.get_messages(session, opts)
if not session then
return Promise.new():resolve(nil)
end

return state.api_client:list_messages(session.id)
return state.api_client:list_messages(session.id, nil, opts)
end

---Get snapshot IDs from a message's parts
Expand Down
2 changes: 1 addition & 1 deletion lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@
---@field fn? fun(args:string[]|nil):nil|Promise<any>|any

---@class OpencodeConfig
---@field preferred_picker 'telescope' | 'fzf' | 'mini.pick' | 'snacks' | 'select' | nil
---@field preferred_picker 'telescope' | 'telescope.nvim' | 'fzf' | 'fzf-lua' | 'mini.pick' | 'snacks' | 'snacks.nvim' | 'select' | nil
---@field default_global_keymaps boolean
---@field default_mode 'build' | 'plan' | string -- Default mode
---@field default_system_prompt string | nil
Expand Down
157 changes: 138 additions & 19 deletions lua/opencode/ui/base_picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ local Promise = require('opencode.promise')
---@field title string|fun(): string The picker title
---@field width? number Optional width for the picker (defaults to config or current window width)
---@field multi_selection? table<string, boolean> Actions that support multi-selection
---@field preview? "file"|"none"|false Preview mode: "file" for file preview, "none" or false to disable
---@field preview? "file"|"custom"|"none"|false Preview mode: "file" for file preview, "custom" for custom preview via preview_fn, "none" or false to disable
---@field preview_fn? fun(item: any, target: PickerPreviewTarget): nil Custom preview function, called when preview = 'custom' and a selection changes
---@field layout_opts? OpencodeUIPickerConfig
---@field close? fun() Close the picker programmatically (set by the backend)

---@class PickerPreviewTarget
---@field get_bufnr fun(self: PickerPreviewTarget): integer?
---@field is_valid fun(self: PickerPreviewTarget): boolean
---@field set_lines fun(self: PickerPreviewTarget, lines: string[]): nil
---@field with_window fun(self: PickerPreviewTarget, fn: fun(): nil): nil

---@class TelescopeEntry
---@field value any
---@field display fun(entry: TelescopeEntry): string[]
Expand Down Expand Up @@ -57,6 +64,64 @@ local Promise = require('opencode.promise')
local M = {}
local picker = require('opencode.ui.picker')

---@param bufnr integer?
---@return PickerPreviewTarget
local function create_buffer_preview_target(bufnr)
return {
get_bufnr = function()
return bufnr
end,
is_valid = function()
return bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr)
end,
set_lines = function(_, lines)
if bufnr == nil or not vim.api.nvim_buf_is_valid(bufnr) then
return
end
local modifiable = vim.bo[bufnr].modifiable
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.bo[bufnr].modifiable = modifiable
end,
with_window = function(_, fn)
if bufnr == nil or not vim.api.nvim_buf_is_valid(bufnr) then
return
end
local win = vim.fn.bufwinid(bufnr)
if win ~= -1 then
vim.api.nvim_win_call(win, fn)
end
end,
}
end

---@param ctx snacks.picker.preview.ctx
---@return PickerPreviewTarget
local function create_snacks_preview_target(ctx)
return {
get_bufnr = function()
return ctx.buf
end,
is_valid = function()
return ctx.buf ~= nil and vim.api.nvim_buf_is_valid(ctx.buf)
end,
set_lines = function(_, lines)
if ctx.preview and ctx.preview.set_lines then
ctx.preview:set_lines(lines)
elseif ctx.buf and vim.api.nvim_buf_is_valid(ctx.buf) then
create_buffer_preview_target(ctx.buf):set_lines(lines)
end
end,
with_window = function(_, fn)
if ctx.win and vim.api.nvim_win_is_valid(ctx.win) then
vim.api.nvim_win_call(ctx.win, fn)
return
end
create_buffer_preview_target(ctx.buf):with_window(fn)
end,
}
end

---Build title with action legend
---@param base_title string The base title
---@param actions table<string, PickerAction> The available actions
Expand Down Expand Up @@ -146,7 +211,22 @@ local function telescope_ui(opts)
prompt_title = opts.title,
finder = finders.new_table({ results = opts.items, entry_maker = make_entry }),
sorter = conf.generic_sorter({}),
previewer = opts.preview == 'file' and require('telescope.previewers').vim_buffer_vimgrep.new({}) or nil,
previewer = (function()
if opts.preview == 'file' then
return require('telescope.previewers').vim_buffer_vimgrep.new({})
elseif opts.preview == 'custom' and opts.preview_fn then
return require('telescope.previewers').new_buffer_previewer({
define_preview = function(self, entry)
if not entry then
return
end
opts.preview_fn(entry.value, create_buffer_preview_target(self.state.bufnr))
end,
})
else
return nil
end
end)(),
layout_config = opts.width and {
width = opts.width + 7, -- extra space for telescope UI
} or nil,
Expand Down Expand Up @@ -252,8 +332,35 @@ local function fzf_ui(opts)
['--delimiter'] = '\x01', -- use SOH as delimiter (invisible char)
},
_headers = { 'actions' },
-- Enable builtin previewer for file preview support
previewer = opts.preview == 'file' and 'builtin' or nil,
previewer = (function()
if opts.preview == 'file' then
return 'builtin'
elseif opts.preview == 'custom' and opts.preview_fn then
return {
_ctor = function()
local previewer = require('fzf-lua.previewer.builtin').buffer_or_file:extend()
function previewer:populate_preview_buf(entry_str)
if not self.win or not self.win:validate_preview() then
return
end
local idx_str = entry_str:match('^(%d+)\x01')
local idx = tonumber(idx_str)
if not idx or not opts.items[idx] then
return
end
-- Create scratch buffer, attach to preview window first
-- so preview_fn can use bufwinid for window-local ops (folds)
local buf = self:get_tmp_buffer()
self:set_preview_buf(buf, true) -- min_winopts=true
opts.preview_fn(opts.items[idx], create_buffer_preview_target(buf))
end
return previewer
end,
}
else
return nil
end
end)(),
fn_fzf_index = function(line)
-- Extract the numeric index prefix before the SOH delimiter
local idx_str = line:match('^(%d+)\x01')
Expand Down Expand Up @@ -490,30 +597,36 @@ end
local function snacks_picker_ui(opts)
local Snacks = require('snacks')

local has_preview = opts.preview == 'file'
local has_custom_preview = opts.preview == 'custom' and opts.preview_fn ~= nil
local has_preview = opts.preview == 'file' or has_custom_preview

local title = type(opts.title) == 'function' and opts.title() or opts.title
---@cast title string

local layout_opts = opts.layout_opts and opts.layout_opts.snacks_layout or nil

local selection_made = false
local default_layout = {
preset = has_custom_preview and 'default' or 'select',
config = function(layout)
local width = opts.width and (opts.width + 3) or nil -- extra space for snacks UI
if not has_preview then
layout.layout.width = width
layout.layout.max_width = width
layout.layout.min_width = width
end
end,
}
if opts.preview == 'file' then
default_layout.preview = 'main'
elseif not has_preview then
default_layout.preview = false
end

---@type snacks.picker.Config
local snack_opts = {
title = title,
layout = layout_opts or {
preview = has_preview and 'main' or false,
preset = 'select',
config = function(layout)
local width = opts.width and (opts.width + 3) or nil -- extra space for snacks UI
if not has_preview then
layout.layout.width = width
layout.layout.max_width = width
layout.layout.min_width = width
end
end,
},
layout = layout_opts or default_layout,
finder = function()
return opts.items
end,
Expand Down Expand Up @@ -560,9 +673,15 @@ local function snacks_picker_ui(opts)
},
}

-- Add file preview if enabled
if has_preview then
if opts.preview == 'file' then
snack_opts.preview = 'file'
elseif has_custom_preview then
snack_opts.preview = function(ctx)
if ctx.item then
ctx.preview:reset()
opts.preview_fn(ctx.item, create_snacks_preview_target(ctx))
end
end
else
snack_opts.preview = function()
return false
Expand Down
27 changes: 20 additions & 7 deletions lua/opencode/ui/output_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -482,22 +482,20 @@ function M.clear_extmarks(start_line, end_line, clear_all)
pcall(vim.api.nvim_buf_clear_namespace, windows.output_buf, clear_all and -1 or M.namespace, start_line, end_line)
end

---Apply extmarks to the output buffer
---Apply extmarks to any buffer (reusable for preview buffers)
---@param bufnr integer Target buffer
---@param extmarks table<number, OutputExtmark[]> Extmarks indexed by line
---@param line_offset? integer Line offset to apply to extmarks, defaults to 0
function M.set_extmarks(extmarks, line_offset)
function M.apply_extmarks(bufnr, extmarks, line_offset)
if not extmarks or type(extmarks) ~= 'table' then
return
end
local windows = state.windows
if not windows or not windows.output_buf or not vim.api.nvim_buf_is_valid(windows.output_buf) then
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
return
end

line_offset = line_offset or 0

local output_buf = windows.output_buf

local line_indices = vim.tbl_keys(extmarks)
table.sort(line_indices)

Expand Down Expand Up @@ -525,11 +523,26 @@ function M.set_extmarks(extmarks, line_offset)
end
end
---@cast m vim.api.keyset.set_extmark
pcall(vim.api.nvim_buf_set_extmark, output_buf, M.namespace, target_line, start_col or 0, m)
pcall(vim.api.nvim_buf_set_extmark, bufnr, M.namespace, target_line, start_col or 0, m)
end
end
end

---Apply extmarks to the output buffer
---@param extmarks table<number, OutputExtmark[]> Extmarks indexed by line
---@param line_offset? integer Line offset to apply to extmarks, defaults to 0
function M.set_extmarks(extmarks, line_offset)
if not extmarks or type(extmarks) ~= 'table' then
return
end
local windows = state.windows
if not windows or not windows.output_buf or not vim.api.nvim_buf_is_valid(windows.output_buf) then
return
end

M.apply_extmarks(windows.output_buf, extmarks, line_offset)
end

---@param start_line integer
---@param end_line integer
function M.highlight_changed_lines(start_line, end_line)
Expand Down
20 changes: 15 additions & 5 deletions lua/opencode/ui/picker.lua
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
local M = {}

local picker_aliases = {
['fzf-lua'] = 'fzf',
['snacks.nvim'] = 'snacks',
['telescope.nvim'] = 'telescope',
}

local function normalize_picker_name(name)
if name == 'select' then
return nil
end

return picker_aliases[name] or name
end

function M.get_best_picker()
local config = require('opencode.config')

local preferred_picker = config.preferred_picker
if preferred_picker and type(preferred_picker) == 'string' and preferred_picker ~= '' then
if preferred_picker == 'select' then
return nil
end

return preferred_picker
return normalize_picker_name(preferred_picker)
end

if pcall(require, 'telescope') then
Expand Down
Loading
Loading