Skip to content

Commit 3617941

Browse files
committed
refactor(formatter): split tool rendering into per-tool modules
Extract each tool formatter into dedicated files and centralize shared helpers to keep formatter.lua smaller and easier to maintain.
1 parent f3fe636 commit 3617941

14 files changed

Lines changed: 491 additions & 369 deletions

File tree

lua/opencode/ui/formatter.lua

Lines changed: 49 additions & 369 deletions
Large diffs are not rendered by default.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
local util = require('opencode.util')
2+
local helpers = require('opencode.ui.formatter.tools.helpers')
3+
4+
local M = {}
5+
6+
---@param ctx table
7+
function M.format(ctx)
8+
local metadata = ctx.metadata or {}
9+
for _, file in ipairs(metadata.files or {}) do
10+
ctx.format_action(ctx.output, 'edit', 'apply patch', file.relativePath or file.filePath, ctx.duration_text)
11+
if ctx.config.ui.output.tools.show_output and file.diff then
12+
local file_type = file and util.get_markdown_filetype(file.filePath) or ''
13+
ctx.format_diff(ctx.output, file.diff, file_type)
14+
end
15+
end
16+
end
17+
18+
---@param _ OpencodeMessagePart
19+
---@param _ table
20+
---@param metadata ApplyPatchToolMetadata
21+
---@return string, string, string
22+
function M.summary(_, _, metadata)
23+
local file = metadata.files and metadata.files[1]
24+
local others_count = metadata.files and #metadata.files - 1 or 0
25+
local suffix = others_count > 0 and string.format(' (+%d more)', others_count) or ''
26+
return 'edit', 'apply patch', file and helpers.resolve_file_name(file.filePath) .. suffix or ''
27+
end
28+
29+
return M
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
local M = {}
2+
3+
---@param ctx table
4+
function M.format(ctx)
5+
ctx.format_action(ctx.output, 'run', 'run', ctx.input and ctx.input.description, ctx.duration_text)
6+
7+
if not ctx.config.ui.output.tools.show_output then
8+
return
9+
end
10+
11+
local input = ctx.input or {}
12+
local metadata = ctx.metadata or {}
13+
if metadata.output or metadata.command or input.command then
14+
local command = input.command or metadata.command or ''
15+
local command_output = metadata.output and metadata.output ~= '' and ('\n' .. metadata.output) or ''
16+
ctx.format_code(ctx.output, vim.split('> ' .. command .. '\n' .. command_output, '\n'), 'bash')
17+
end
18+
end
19+
20+
---@param _ OpencodeMessagePart
21+
---@param input BashToolInput
22+
---@return string, string, string
23+
function M.summary(_, input)
24+
return 'run', 'run', input.description or ''
25+
end
26+
27+
return M
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
local util = require('opencode.util')
2+
local helpers = require('opencode.ui.formatter.tools.helpers')
3+
4+
local M = {}
5+
6+
---@param ctx table
7+
function M.format(ctx)
8+
local input = ctx.input or {}
9+
local metadata = ctx.metadata or {}
10+
local tool_output = ctx.tool_output
11+
local tool_type = ctx.tool_type
12+
13+
local file_name = tool_type == 'read' and helpers.resolve_display_file_name(input.filePath or '', tool_output)
14+
or helpers.resolve_file_name(input.filePath or '')
15+
16+
local file_type = input.filePath and util.get_markdown_filetype(input.filePath) or ''
17+
18+
ctx.format_action(ctx.output, tool_type, tool_type, file_name, ctx.duration_text)
19+
20+
if not ctx.config.ui.output.tools.show_output then
21+
return
22+
end
23+
24+
if tool_type == 'edit' and metadata.diff then
25+
ctx.format_diff(ctx.output, metadata.diff, file_type)
26+
elseif tool_type == 'write' and input.content then
27+
ctx.format_code(ctx.output, vim.split(input.content, '\n'), file_type)
28+
end
29+
end
30+
31+
---@param part OpencodeMessagePart
32+
---@param input FileToolInput
33+
---@return string, string, string
34+
function M.summary(part, input)
35+
local tool = part.tool
36+
if tool == 'read' then
37+
local tool_output = part.state and part.state.output or nil
38+
return 'read', 'read', helpers.resolve_display_file_name(input.filePath, tool_output)
39+
end
40+
return tool, tool, helpers.resolve_file_name(input.filePath)
41+
end
42+
43+
return M
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
local M = {}
2+
3+
---@param ctx table
4+
function M.format(ctx)
5+
local input = ctx.input or {}
6+
local metadata = ctx.metadata or {}
7+
8+
ctx.format_action(ctx.output, 'search', 'glob', input.pattern, ctx.duration_text)
9+
if not ctx.config.ui.output.tools.show_output then
10+
return
11+
end
12+
13+
local prefix = metadata.truncated and ' more than' or ''
14+
ctx.output:add_line(string.format('Found%s `%d` file(s):', prefix, metadata.count or 0))
15+
end
16+
17+
---@param _ OpencodeMessagePart
18+
---@param input GlobToolInput
19+
---@return string, string, string
20+
function M.summary(_, input)
21+
return 'search', 'glob', input.pattern or ''
22+
end
23+
24+
return M
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
local helpers = require('opencode.ui.formatter.tools.helpers')
2+
3+
local M = {}
4+
5+
---@param ctx table
6+
function M.format(ctx)
7+
local metadata = ctx.metadata or {}
8+
local grep_str = helpers.resolve_grep_string(ctx.input)
9+
10+
ctx.format_action(ctx.output, 'search', 'grep', grep_str, ctx.duration_text)
11+
if not ctx.config.ui.output.tools.show_output then
12+
return
13+
end
14+
15+
local prefix = metadata.truncated and ' more than' or ''
16+
ctx.output:add_line(
17+
string.format('Found%s `%d` match' .. (metadata.matches ~= 1 and 'es' or ''), prefix, metadata.matches or 0)
18+
)
19+
end
20+
21+
---@param _ OpencodeMessagePart
22+
---@param input GrepToolInput
23+
---@return string, string, string
24+
function M.summary(_, input)
25+
return 'search', 'grep', helpers.resolve_grep_string(input)
26+
end
27+
28+
return M
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
local M = {}
2+
3+
---@param file_path string
4+
---@return string
5+
function M.resolve_file_name(file_path)
6+
if not file_path or file_path == '' then
7+
return ''
8+
end
9+
local cwd = vim.fn.getcwd()
10+
local absolute = vim.fn.fnamemodify(file_path, ':p')
11+
if vim.startswith(absolute, cwd .. '/') then
12+
return absolute:sub(#cwd + 2)
13+
end
14+
return absolute
15+
end
16+
17+
---@param file_path string
18+
---@param tool_output? string
19+
---@return boolean
20+
function M.is_directory_path(file_path, tool_output)
21+
if not file_path or file_path == '' then
22+
return false
23+
end
24+
25+
if vim.endswith(file_path, '/') then
26+
return true
27+
end
28+
29+
return type(tool_output) == 'string' and tool_output:match('<type>directory</type>') ~= nil
30+
end
31+
32+
---@param file_path string
33+
---@param tool_output? string
34+
---@return string
35+
function M.resolve_display_file_name(file_path, tool_output)
36+
local resolved = M.resolve_file_name(file_path)
37+
38+
if resolved ~= '' and M.is_directory_path(file_path, tool_output) and not vim.endswith(resolved, '/') then
39+
resolved = resolved .. '/'
40+
end
41+
42+
return resolved
43+
end
44+
45+
---@param input GrepToolInput|nil
46+
---@return string
47+
function M.resolve_grep_string(input)
48+
if not input then
49+
return ''
50+
end
51+
local path_part = input.path or input.include or ''
52+
local pattern_part = input.pattern or ''
53+
return table.concat(
54+
vim.tbl_filter(function(p)
55+
return p ~= nil and p ~= ''
56+
end, { path_part, pattern_part }),
57+
' '
58+
)
59+
end
60+
61+
return M
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
return {
2+
bash = require('opencode.ui.formatter.tools.bash'),
3+
read = require('opencode.ui.formatter.tools.file'),
4+
edit = require('opencode.ui.formatter.tools.file'),
5+
write = require('opencode.ui.formatter.tools.file'),
6+
apply_patch = require('opencode.ui.formatter.tools.apply_patch'),
7+
todowrite = require('opencode.ui.formatter.tools.todowrite'),
8+
glob = require('opencode.ui.formatter.tools.glob'),
9+
grep = require('opencode.ui.formatter.tools.grep'),
10+
webfetch = require('opencode.ui.formatter.tools.webfetch'),
11+
list = require('opencode.ui.formatter.tools.list'),
12+
question = require('opencode.ui.formatter.tools.question'),
13+
task = require('opencode.ui.formatter.tools.task'),
14+
tool = require('opencode.ui.formatter.tools.tool'),
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
local M = {}
2+
3+
---@param ctx table
4+
function M.format(ctx)
5+
local input = ctx.input or {}
6+
local metadata = ctx.metadata or {}
7+
local tool_output = ctx.tool_output
8+
9+
ctx.format_action(ctx.output, 'list', 'list', input.path or '', ctx.duration_text)
10+
if not ctx.config.ui.output.tools.show_output then
11+
return
12+
end
13+
14+
local lines = vim.split(vim.trim(tool_output or ''), '\n')
15+
if #lines < 1 or metadata.count == 0 then
16+
ctx.output:add_line('No files found.')
17+
return
18+
end
19+
if #lines > 1 then
20+
ctx.output:add_line('Files:')
21+
for i = 2, #lines do
22+
local file = vim.trim(lines[i])
23+
if file ~= '' then
24+
ctx.output:add_line('' .. file)
25+
end
26+
end
27+
end
28+
if metadata.truncated then
29+
ctx.output:add_line(string.format('Results truncated, showing first %d files', metadata.count or '?'))
30+
end
31+
end
32+
33+
---@param _ OpencodeMessagePart
34+
---@param input ListToolInput
35+
---@return string, string, string
36+
function M.summary(_, input)
37+
return 'list', 'list', input.path or ''
38+
end
39+
40+
return M
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
local M = {}
2+
3+
---@param ctx table
4+
function M.format(ctx)
5+
local input = ctx.input or {}
6+
local metadata = ctx.metadata or {}
7+
8+
ctx.format_action(ctx.output, 'question', 'question', '', ctx.duration_text)
9+
ctx.output:add_empty_line()
10+
if not ctx.config.ui.output.tools.show_output or ctx.status ~= 'completed' then
11+
return
12+
end
13+
14+
local questions = input.questions or {}
15+
local answers = metadata.answers or {}
16+
17+
for i, question in ipairs(questions) do
18+
local question_lines = vim.split(question.question, '\n')
19+
if #question_lines > 1 then
20+
ctx.output:add_line(string.format('**Q%d:** %s', i, question.header))
21+
for _, line in ipairs(question_lines) do
22+
ctx.output:add_line(line)
23+
end
24+
else
25+
ctx.output:add_line(string.format('**Q%d:** %s', i, question_lines[1]))
26+
end
27+
28+
local answer = answers[i] and answers[i][1] or 'No answer'
29+
local answer_lines = vim.split(answer, '\n', { plain = true })
30+
ctx.output:add_line(string.format('**A%d:** %s', i, answer_lines[1]))
31+
for line_idx = 2, #answer_lines do
32+
ctx.output:add_line(answer_lines[line_idx])
33+
end
34+
35+
if i < #questions then
36+
ctx.output:add_line('')
37+
end
38+
end
39+
end
40+
41+
return M

0 commit comments

Comments
 (0)