From 4c6a7d454ce91c90ceb509f84391d091358fc737 Mon Sep 17 00:00:00 2001 From: Joshua Date: Sun, 18 Jan 2026 20:10:16 -0600 Subject: [PATCH 1/2] feat(fzf): add fzf-lua integration for buffer and file selection --- lua/opencode.lua | 1 + lua/opencode/config.lua | 21 +++++ lua/opencode/ui/fzf.lua | 195 ++++++++++++++++++++++++++++++++++++++++ plugin/fzf.lua | 34 +++++++ 4 files changed, 251 insertions(+) create mode 100644 lua/opencode/ui/fzf.lua create mode 100644 plugin/fzf.lua diff --git a/lua/opencode.lua b/lua/opencode.lua index 1e6880b1..91ac5835 100644 --- a/lua/opencode.lua +++ b/lua/opencode.lua @@ -3,6 +3,7 @@ local M = {} M.ask = require("opencode.ui.ask").ask M.select = require("opencode.ui.select").select +M.fzf = require("opencode.ui.fzf") M.prompt = require("opencode.api.prompt").prompt M.operator = require("opencode.api.operator").operator diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 2b1a3d78..316e6446 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -32,6 +32,9 @@ vim.g.opencode_opts = vim.g.opencode_opts ---Supports [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md). ---@field select? opencode.select.Opts --- +---Options for `fzf` integration. +---@field fzf? opencode.fzf.Opts +--- ---Options for `opencode` event handling. ---@field events? opencode.events.Opts --- @@ -42,6 +45,19 @@ vim.g.opencode_opts = vim.g.opencode_opts ---@field prompt string The prompt to send to `opencode`. ---@field ask? boolean Call `ask(prompt)` instead of `prompt(prompt)`. Useful for prompts that expect additional user input. +---@class opencode.fzf.Opts +--- +---Options for file search using fzf-lua. +---Passed to fzf-lua's files() function. +---@field files? table +--- +---Options for buffer search using fzf-lua. +---Passed to fzf-lua's buffers() function. +---@field buffers? table +--- +---Default prompt prefix when selecting files/buffers. +---@field prompt_prefix? string + ---@type opencode.Opts local defaults = { port = nil, @@ -108,6 +124,11 @@ local defaults = { }, }, }, + fzf = { + files = {}, + buffers = {}, + prompt_prefix = "", + }, events = { enabled = true, reload = true, diff --git a/lua/opencode/ui/fzf.lua b/lua/opencode/ui/fzf.lua new file mode 100644 index 00000000..0f92176c --- /dev/null +++ b/lua/opencode/ui/fzf.lua @@ -0,0 +1,195 @@ +local M = {} + +local config = require("opencode.config").opts.fzf or {} + +local function is_buf_valid(buf) + return vim.api.nvim_get_option_value("buftype", { buf = buf }) == "" and vim.api.nvim_buf_get_name(buf) ~= "" +end + +local function strip_ansi_codes(str) + local result = str + result = result:gsub("\27%[[0-9;]*m", "") + result = result:gsub("\27%[K", "") + return result +end + +local function strip_file_icon(str) + local stripped = strip_ansi_codes(str) + stripped = stripped:gsub("^%s*", "") + local icon_and_space = stripped:match("^[^\32-\126]+%s+") + if icon_and_space then + stripped = stripped:sub(#icon_and_space + 1) + end + return stripped:match("^%s*(.-)%s*$") +end + +local function format_file_for_opencode(filepath) + local rel_path = vim.fn.fnamemodify(filepath, ":.") + return "@" .. rel_path .. " " +end + +local function send_files_to_opencode(files, prompt_prefix) + if not files or #files == 0 then + return + end + + local file_contexts = {} + for _, file in ipairs(files) do + local clean_file = strip_file_icon(file) + if clean_file and clean_file ~= "" then + table.insert(file_contexts, format_file_for_opencode(clean_file)) + end + end + + if #file_contexts == 0 then + return + end + + local prompt = table.concat(file_contexts, " ") + if prompt_prefix and #prompt_prefix > 0 then + prompt = prompt_prefix .. " " .. prompt + end + + require("opencode").prompt(prompt) +end + +function M.select_buffers(prompt_prefix) + local ok, fzf = pcall(require, "fzf-lua") + if not ok then + vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR) + return + end + + local buffers = {} + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if is_buf_valid(buf) then + local bufname = vim.api.nvim_buf_get_name(buf) + table.insert(buffers, { + path = bufname, + bufnr = buf, + ordinal = bufname, + display = bufname, + }) + end + end + + if #buffers == 0 then + vim.notify("No valid buffers found", vim.log.levels.WARN) + return + end + + local opts = vim.tbl_deep_extend("force", { + prompt = "Select buffers> ", + previewer = false, + file_icons = false, + git_icons = false, + fzf_opts = { + ["--multi"] = "", + ["--header"] = "Tab to select multiple, Enter to confirm", + }, + actions = { + ["default"] = function(selected) + if not selected or #selected == 0 then + return + end + send_files_to_opencode(selected, prompt_prefix or config.prompt_prefix) + end, + }, + }, config.buffers or {}) + + fzf.fzf_exec( + function(cb) + for _, buf in ipairs(buffers) do + cb(buf.display) + end + cb(nil) + end, + opts + ) +end + +function M.select_files(prompt_prefix, opts) + opts = opts or {} + + local ok, fzf = pcall(require, "fzf-lua") + if not ok then + vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR) + return + end + + local files_opts = vim.tbl_deep_extend("force", { + prompt = "Select files> ", + previewer = "builtin", + file_icons = false, + color_icons = false, + git_icons = false, + fzf_opts = { + ["--multi"] = "", + ["--header"] = "Tab to select multiple, Enter to confirm", + }, + actions = { + ["default"] = function(selected) + if not selected or #selected == 0 then + return + end + send_files_to_opencode(selected, prompt_prefix or config.prompt_prefix) + end, + }, + }, config.files or {}) + + if opts.cwd then + files_opts.cwd = opts.cwd + end + + fzf.files(files_opts) +end + +function M.ask_with_files(prompt_text) + local ok, fzf = pcall(require, "fzf-lua") + if not ok then + vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR) + return + end + + local function show_file_selector() + vim.ui.select({ "buffers", "project files" }, { prompt = "Select source: " }, function(choice) + if not choice then + return + end + + if choice == "buffers" then + M.select_buffers(prompt_text or config.prompt_prefix) + elseif choice == "project files" then + M.select_files(prompt_text or config.prompt_prefix) + end + end) + end + + show_file_selector() +end + +function M.append_files_to_current_prompt() + local ok, fzf = pcall(require, "fzf-lua") + if not ok then + vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR) + return + end + + local function show_file_selector() + vim.ui.select({ "buffers", "project files" }, { prompt = "Select source: " }, function(choice) + if not choice then + return + end + + if choice == "buffers" then + M.select_buffers(config.prompt_prefix) + elseif choice == "project files" then + M.select_files(config.prompt_prefix) + end + end) + end + + show_file_selector() +end + +return M diff --git a/plugin/fzf.lua b/plugin/fzf.lua new file mode 100644 index 00000000..88a3875f --- /dev/null +++ b/plugin/fzf.lua @@ -0,0 +1,34 @@ +if not pcall(require, "fzf-lua") then + return +end + +vim.api.nvim_create_user_command("OpencodeFzfBuffers", function(opts) + local prompt_prefix = opts.fargs[1] or "" + require("opencode").fzf.select_buffers(prompt_prefix) +end, { + nargs = "?", + desc = "Select buffers using fzf-lua and send to opencode", +}) + +vim.api.nvim_create_user_command("OpencodeFzfFiles", function(opts) + local prompt_prefix = opts.fargs[1] or "" + require("opencode").fzf.select_files(prompt_prefix) +end, { + nargs = "?", + desc = "Select project files using fzf-lua and send to opencode", +}) + +vim.api.nvim_create_user_command("OpencodeFzfAsk", function(opts) + local prompt_text = opts.fargs[1] or "" + require("opencode").fzf.ask_with_files(prompt_text) +end, { + nargs = "?", + desc = "Ask opencode with files selected via fzf-lua", +}) + +vim.api.nvim_create_user_command("OpencodeFzfAppend", function() + require("opencode").fzf.append_files_to_current_prompt() +end, { + nargs = 0, + desc = "Append files selected via fzf-lua to current opencode prompt", +}) From aa454687b1d449002f7a634d7b2e3bf70d067246 Mon Sep 17 00:00:00 2001 From: xeon826 Date: Sun, 18 Jan 2026 20:45:41 -0600 Subject: [PATCH 2/2] fix(ui): ensure normal mode before opening fzf prompts to prevent insert mode conflicts --- lua/opencode/ui/fzf.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/opencode/ui/fzf.lua b/lua/opencode/ui/fzf.lua index 0f92176c..ce44052c 100644 --- a/lua/opencode/ui/fzf.lua +++ b/lua/opencode/ui/fzf.lua @@ -6,6 +6,14 @@ local function is_buf_valid(buf) return vim.api.nvim_get_option_value("buftype", { buf = buf }) == "" and vim.api.nvim_buf_get_name(buf) ~= "" end +local function escape_terminal_mode() + local mode = vim.api.nvim_get_mode().mode + if mode:match("^[it]") then + vim.cmd("stopinsert") + vim.fn.feedkeys("\27", "n") + end +end + local function strip_ansi_codes(str) local result = str result = result:gsub("\27%[[0-9;]*m", "") @@ -54,6 +62,8 @@ local function send_files_to_opencode(files, prompt_prefix) end function M.select_buffers(prompt_prefix) + escape_terminal_mode() + local ok, fzf = pcall(require, "fzf-lua") if not ok then vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR) @@ -109,6 +119,7 @@ function M.select_buffers(prompt_prefix) end function M.select_files(prompt_prefix, opts) + escape_terminal_mode() opts = opts or {} local ok, fzf = pcall(require, "fzf-lua") @@ -145,6 +156,8 @@ function M.select_files(prompt_prefix, opts) end function M.ask_with_files(prompt_text) + escape_terminal_mode() + local ok, fzf = pcall(require, "fzf-lua") if not ok then vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR) @@ -169,6 +182,8 @@ function M.ask_with_files(prompt_text) end function M.append_files_to_current_prompt() + escape_terminal_mode() + local ok, fzf = pcall(require, "fzf-lua") if not ok then vim.notify("fzf-lua not found. Please install fzf-lua to use this feature.", vim.log.levels.ERROR)