297 lines
6.8 KiB
Nix
297 lines
6.8 KiB
Nix
{pkgs, ...}: let
|
||
opencode-nvim =
|
||
pkgs.vimUtils.buildVimPlugin {
|
||
pname = "opencode-nvim";
|
||
version = "unstable-2026-03-07";
|
||
src =
|
||
pkgs.fetchFromGitHub {
|
||
owner = "sudo-tee";
|
||
repo = "opencode.nvim";
|
||
rev = "dffa3f39a8251c7ba53b1544d8536b5f51b4e90d";
|
||
hash = "sha256-KxIuToMpyo/Yi4xKirMV8Fznlma6EL1k4YQm5MQdGw4=";
|
||
};
|
||
doCheck = false;
|
||
};
|
||
in {
|
||
programs.nixvim = {
|
||
extraPlugins = with pkgs.vimPlugins; [
|
||
opencode-nvim
|
||
plenary-nvim
|
||
render-markdown-nvim
|
||
];
|
||
extraConfigLua = ''
|
||
local api = vim.api
|
||
local opencode_output_filetype = 'opencode_output'
|
||
local opencode_window_filetypes = {
|
||
opencode = true,
|
||
opencode_output = true,
|
||
}
|
||
|
||
local palette = {
|
||
base = '#eff1f5',
|
||
mantle = '#e6e9ef',
|
||
surface0 = '#ccd0da',
|
||
surface1 = '#bcc0cc',
|
||
text = '#4c4f69',
|
||
subtext0 = '#6c6f85',
|
||
overlay0 = '#9ca0b0',
|
||
blue = '#1e66f5',
|
||
lavender = '#7287fd',
|
||
sapphire = '#209fb5',
|
||
teal = '#179299',
|
||
green = '#40a02b',
|
||
mauve = '#8839ef',
|
||
peach = '#fe640b',
|
||
}
|
||
|
||
local function set_highlights(highlights)
|
||
for group, spec in pairs(highlights) do
|
||
api.nvim_set_hl(0, group, spec)
|
||
end
|
||
end
|
||
|
||
local opencode_markdown_conceal_query = vim.treesitter.query.parse('markdown_inline', [[
|
||
[
|
||
(emphasis_delimiter)
|
||
(code_span_delimiter)
|
||
(latex_span_delimiter)
|
||
] @conceal
|
||
|
||
(inline_link
|
||
[
|
||
"["
|
||
"]"
|
||
"("
|
||
(link_destination)
|
||
")"
|
||
] @conceal)
|
||
|
||
(full_reference_link
|
||
[
|
||
"["
|
||
"]"
|
||
(link_label)
|
||
] @conceal)
|
||
|
||
(collapsed_reference_link
|
||
[
|
||
"["
|
||
"]"
|
||
] @conceal)
|
||
|
||
(shortcut_link
|
||
[
|
||
"["
|
||
"]"
|
||
] @conceal)
|
||
|
||
(image
|
||
[
|
||
"!"
|
||
"["
|
||
"]"
|
||
"("
|
||
(link_destination)
|
||
")"
|
||
] @conceal)
|
||
]])
|
||
|
||
local function collect_conceal_marks(ctx)
|
||
local marks = {}
|
||
|
||
for _, node in opencode_markdown_conceal_query:iter_captures(ctx.root, ctx.buf) do
|
||
local start_row, start_col, end_row, end_col = node:range()
|
||
marks[#marks + 1] = {
|
||
conceal = true,
|
||
start_row = start_row,
|
||
start_col = start_col,
|
||
opts = {
|
||
end_row = end_row,
|
||
end_col = end_col,
|
||
conceal = "",
|
||
},
|
||
}
|
||
end
|
||
|
||
return marks
|
||
end
|
||
|
||
local function set_opencode_output_conceal()
|
||
if vim.bo.filetype ~= opencode_output_filetype then
|
||
return
|
||
end
|
||
|
||
vim.wo.conceallevel = 3
|
||
vim.wo.concealcursor = 'nvic'
|
||
end
|
||
|
||
local function hide_opencode_statusline()
|
||
if not opencode_window_filetypes[vim.bo.filetype] then
|
||
return
|
||
end
|
||
|
||
vim.wo.statusline = ' '
|
||
end
|
||
|
||
vim.treesitter.language.register('markdown', opencode_output_filetype)
|
||
vim.treesitter.language.register('markdown_inline', opencode_output_filetype)
|
||
|
||
api.nvim_create_autocmd({ 'FileType', 'BufWinEnter', 'WinEnter' }, {
|
||
callback = set_opencode_output_conceal,
|
||
})
|
||
api.nvim_create_autocmd({ 'FileType', 'BufWinEnter', 'WinEnter', 'BufEnter' }, {
|
||
pattern = '*',
|
||
callback = hide_opencode_statusline,
|
||
})
|
||
|
||
set_highlights({
|
||
RenderMarkdownCode = { bg = palette.mantle },
|
||
RenderMarkdownCodeBorder = { fg = palette.surface0, bg = palette.mantle },
|
||
RenderMarkdownCodeInline = { bg = palette.mantle },
|
||
RenderMarkdownH1 = { fg = palette.blue, bold = true },
|
||
RenderMarkdownH2 = { fg = palette.mauve, bold = true },
|
||
RenderMarkdownH3 = { fg = palette.teal, bold = true },
|
||
RenderMarkdownH4 = { fg = palette.peach, bold = true },
|
||
OpencodeInputLegend = { fg = palette.subtext0, bold = true },
|
||
OpencodeAgentBuild = { bg = palette.overlay0, fg = palette.base, bold = true },
|
||
})
|
||
|
||
local render_markdown_config = {
|
||
anti_conceal = { enabled = false },
|
||
heading = {
|
||
icons = { '◆ ', '◇ ', '○ ', '· ', '· ', '· ' },
|
||
backgrounds = {},
|
||
position = 'inline',
|
||
width = 'block',
|
||
left_pad = 0,
|
||
right_pad = 2,
|
||
border = false,
|
||
sign = false,
|
||
},
|
||
code = {
|
||
sign = false,
|
||
width = 'full',
|
||
left_pad = 2,
|
||
right_pad = 0,
|
||
border = 'thin',
|
||
language_icon = false,
|
||
language_name = true,
|
||
},
|
||
bullet = {
|
||
icons = { '·', '–', '·', '–' },
|
||
},
|
||
custom_handlers = {
|
||
markdown_inline = {
|
||
extends = true,
|
||
parse = collect_conceal_marks,
|
||
},
|
||
},
|
||
file_types = { opencode_output_filetype },
|
||
win_options = {
|
||
conceallevel = { rendered = 3 },
|
||
concealcursor = { rendered = 'nvic' },
|
||
},
|
||
}
|
||
|
||
require('render-markdown').setup(render_markdown_config)
|
||
|
||
local opencode_icon_overrides = {
|
||
header_user = '│',
|
||
header_assistant = '│',
|
||
run = '▸',
|
||
task = '◦',
|
||
read = '◦',
|
||
edit = '◦',
|
||
write = '◦',
|
||
plan = '◦',
|
||
search = '◦',
|
||
web = '◦',
|
||
list = '◦',
|
||
tool = '◦',
|
||
snapshot = '◦',
|
||
restore_point = '◦',
|
||
file = '·',
|
||
folder = '·',
|
||
attached_file = '·',
|
||
agent = '·',
|
||
reference = '·',
|
||
reasoning = '◦',
|
||
question = '?',
|
||
border = '│',
|
||
}
|
||
|
||
require('opencode').setup({
|
||
server = {
|
||
url = 'http://127.0.0.1',
|
||
port = 18822,
|
||
auto_kill = false,
|
||
},
|
||
input = {
|
||
text = {
|
||
wrap = true,
|
||
},
|
||
},
|
||
debug = {
|
||
show_ids = false,
|
||
},
|
||
ui = {
|
||
icons = {
|
||
preset = 'nerdfonts',
|
||
overrides = opencode_icon_overrides,
|
||
},
|
||
},
|
||
})
|
||
|
||
do
|
||
local config = require('opencode.config')
|
||
local formatter = require('opencode.ui.formatter')
|
||
local format_utils = require('opencode.ui.formatter.utils')
|
||
local icons = require('opencode.ui.icons')
|
||
local util = require('opencode.util')
|
||
|
||
local function format_reasoning_title(part)
|
||
local title = 'Reasoning'
|
||
local time = part.time
|
||
|
||
if time and type(time) == 'table' and time.start then
|
||
local duration_text = util.format_duration_seconds(time.start, time['end'])
|
||
if duration_text then
|
||
title = string.format('%s %s', title, duration_text)
|
||
end
|
||
end
|
||
|
||
return title
|
||
end
|
||
|
||
local function highlight_reasoning_block(output, start_line)
|
||
local end_line = output:get_line_count()
|
||
|
||
if end_line - start_line > 1 then
|
||
formatter.add_vertical_border(output, start_line, end_line, 'OpencodeToolBorder', -1, 'OpencodeReasoningText')
|
||
return
|
||
end
|
||
|
||
output:add_extmark(start_line - 1, {
|
||
line_hl_group = 'OpencodeReasoningText',
|
||
})
|
||
end
|
||
|
||
formatter._format_reasoning = function(output, part)
|
||
local text = vim.trim(part.text or "")
|
||
local start_line = output:get_line_count() + 1
|
||
|
||
format_utils.format_action(output, icons.get('reasoning'), format_reasoning_title(part), "")
|
||
|
||
if config.ui.output.tools.show_reasoning_output and text ~= "" then
|
||
output:add_empty_line()
|
||
output:add_lines(vim.split(text, '\n'), ' ')
|
||
output:add_empty_line()
|
||
end
|
||
|
||
highlight_reasoning_block(output, start_line)
|
||
end
|
||
end
|
||
'';
|
||
};
|
||
}
|