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
116 changes: 87 additions & 29 deletions lua/opencode/ui/formatter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -939,40 +939,98 @@ function M._format_code(output, lines, language)
output:add_line('`````')
end

---@param output Output Output object to write to
---@param code string
---@param file_type string
---@param lines string[]
local function parse_diff_line_numbers(lines)
local numbered_lines = {}
local old_line
local new_line
local max_line_number = 0

for idx, line in ipairs(lines) do
local old_start, new_start = line:match('^@@ %-(%d+),?%d* %+(%d+),?%d* @@')

if old_start and new_start then
old_line = tonumber(old_start)
new_line = tonumber(new_start)
elseif old_line and new_line then
local first_char = line:sub(1, 1)

if first_char == ' ' then
numbered_lines[idx] = { old = old_line, new = new_line }
max_line_number = math.max(max_line_number, old_line, new_line)
old_line = old_line + 1
new_line = new_line + 1
elseif first_char == '+' and not line:match('^%+%+%+%s') then
numbered_lines[idx] = { old = nil, new = new_line }
max_line_number = math.max(max_line_number, new_line)
new_line = new_line + 1
elseif first_char == '-' and not line:match('^%-%-%-%s') then
numbered_lines[idx] = { old = old_line, new = nil }
max_line_number = math.max(max_line_number, old_line)
old_line = old_line + 1
end
end
end

return numbered_lines, #tostring(max_line_number)
end

local function build_diff_gutter(line_numbers, width)
local line_number = line_numbers.new or line_numbers.old
return string.format('%-' .. width .. 's', line_number and tostring(line_number) or '')
end

local function add_diff_line(output, line, line_numbers, width)
local first_char = line:sub(1, 1)
local line_hl = first_char == '+' and 'OpencodeDiffAdd' or first_char == '-' and 'OpencodeDiffDelete' or nil
local gutter_hl = first_char == '+' and 'OpencodeDiffAddGutter'
or first_char == '-' and 'OpencodeDiffDeleteGutter'
or 'OpencodeDiffGutter'
local sign_hl = gutter_hl
local gutter = build_diff_gutter(line_numbers, width)
local gutter_width = #gutter + 2

output:add_line(string.rep(' ', gutter_width) .. line:sub(2))

local line_idx = output:get_line_count()
local extmark = {
end_col = 0,
end_row = line_idx,
virt_text = {
{ gutter, gutter_hl },
{ first_char, sign_hl },
{ ' ', gutter_hl },
},
priority = 5000,
right_gravity = true,
end_right_gravity = false,
virt_text_hide = false,
virt_text_pos = 'overlay',
virt_text_repeat_linebreak = false,
}

if line_hl then
extmark.hl_group = line_hl
extmark.hl_eol = true
end

output:add_extmark(line_idx - 1, extmark --[[@as OutputExtmark]])
end

function M.format_diff(output, code, file_type)
output:add_empty_line()

--- NOTE: use longer code fence because code could contain ```
output:add_line('`````' .. file_type)
local lines = vim.split(code, '\n')
if #lines > 5 then
lines = vim.list_slice(lines, 6)
end

for _, line in ipairs(lines) do
local first_char = line:sub(1, 1)
if first_char == '+' or first_char == '-' then
local hl_group = first_char == '+' and 'OpencodeDiffAdd' or 'OpencodeDiffDelete'
output:add_line(' ' .. line:sub(2))
local line_idx = output:get_line_count()
output:add_extmark(line_idx - 1, function()
return {
end_col = 0,
end_row = line_idx,
virt_text = { { first_char, hl_group } },
hl_group = hl_group,
hl_eol = true,
priority = 5000,
right_gravity = true,
end_right_gravity = false,
virt_text_hide = false,
virt_text_pos = 'overlay',
virt_text_repeat_linebreak = false,
}
end)
local full_lines = vim.split(code, '\n')
local numbered_lines, line_number_width = parse_diff_line_numbers(full_lines)
local first_visible_line = #full_lines > 5 and 6 or 1
local lines = first_visible_line > 1 and vim.list_slice(full_lines, first_visible_line) or full_lines

for idx, line in ipairs(lines) do
local source_idx = first_visible_line + idx - 1
if numbered_lines[source_idx] then
add_diff_line(output, line, numbered_lines[source_idx], line_number_width)
else
output:add_line(line)
end
Expand Down
6 changes: 6 additions & 0 deletions lua/opencode/ui/highlight.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ function M.setup()
vim.api.nvim_set_hl(0, 'OpencodeDiffDelete', { bg = '#FFEBEE', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffAddText', { link = 'Added', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffDeleteText', { link = 'Removed', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffGutter', { fg = '#757575', bg = '#F5F5F5', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffAddGutter', { fg = '#2E7D32', bg = '#F1FAF1', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffDeleteGutter', { fg = '#C62828', bg = '#FFF1F3', default = true })
vim.api.nvim_set_hl(0, 'OpencodeRevertBorder', { bg = '#FF9E3B', default = true })
vim.api.nvim_set_hl(0, 'OpencodePermissionBorder', { fg = '#FF9E3B', nocombine = true, default = true })
vim.api.nvim_set_hl(0, 'OpencodeAgentPlan', { bg = '#2196F3', fg = '#FFFFFF', bold = true, default = true })
Expand Down Expand Up @@ -58,6 +61,9 @@ function M.setup()
vim.api.nvim_set_hl(0, 'OpencodeDiffDelete', { bg = '#43242B', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffAddText', { link = 'Added', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffDeleteText', { link = 'Removed', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffGutter', { fg = '#6B7280', bg = '#252631', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffAddGutter', { fg = '#A5D6A7', bg = '#344032', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDiffDeleteGutter', { fg = '#EF9A9A', bg = '#52303A', default = true })
vim.api.nvim_set_hl(0, 'OpencodeAgentPlan', { bg = '#61AFEF', fg = '#FFFFFF', bold = true, default = true })
vim.api.nvim_set_hl(0, 'OpencodeAgentBuild', { bg = '#616161', fg = '#FFFFFF', bold = true, default = true })
vim.api.nvim_set_hl(0, 'OpencodeAgentCustom', { bg = '#3b4261', fg = '#FFFFFF', bold = true, default = true })
Expand Down
217 changes: 216 additions & 1 deletion tests/data/apply-patch.expected.json
Original file line number Diff line number Diff line change
@@ -1 +1,216 @@
{"actions":[],"lines":["----","","","----","","","** apply patch** `src/app/features/auth/__tests__/LoginForm.test.tsx` 4s","","`````tsx"," import React from 'react'"," // minimal diff for testing","","`````","",""],"timestamp":1772538345,"extmarks":[[1,1,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" [msg_user001]","OpencodeHint"]],"virt_text_pos":"win_col","ns_id":3,"priority":10,"virt_text_win_col":-3,"virt_text_repeat_linebreak":false}],[2,4,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],["","OpencodeHint"],[" [msg_asst001]","OpencodeHint"]],"virt_text_pos":"win_col","ns_id":3,"priority":10,"virt_text_win_col":-3,"virt_text_repeat_linebreak":false}],[3,6,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}],[4,7,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}],[5,8,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}],[6,9,0,{"virt_text_pos":"overlay","end_row":10,"priority":5000,"right_gravity":true,"end_right_gravity":false,"hl_group":"OpencodeDiffAdd","end_col":0,"hl_eol":true,"ns_id":3,"virt_text_hide":false,"virt_text":[["+","OpencodeDiffAdd"]],"virt_text_repeat_linebreak":false}],[7,9,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}],[8,10,0,{"virt_text_pos":"overlay","end_row":11,"priority":5000,"right_gravity":true,"end_right_gravity":false,"hl_group":"OpencodeDiffAdd","end_col":0,"hl_eol":true,"ns_id":3,"virt_text_hide":false,"virt_text":[["+","OpencodeDiffAdd"]],"virt_text_repeat_linebreak":false}],[9,10,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}],[10,11,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}],[11,12,0,{"right_gravity":true,"virt_text_hide":false,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","ns_id":3,"priority":4096,"virt_text_win_col":-1,"virt_text_repeat_linebreak":true}]]}
{
"actions": [],
"extmarks": [
[
1,
1,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 10,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -3,
"virt_text_repeat_linebreak": false,
"virt_text": [
["▌󰭻 ", "OpencodeMessageRoleUser"],
[" "],
["USER", "OpencodeMessageRoleUser"],
["", "OpencodeHint"],
[" [msg_user001]", "OpencodeHint"]
]
}
],
[
2,
4,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 10,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -3,
"virt_text_repeat_linebreak": false,
"virt_text": [
[" ", "OpencodeMessageRoleAssistant"],
[" "],
["BUILD", "OpencodeMessageRoleAssistant"],
["", "OpencodeHint"],
[" [msg_asst001]", "OpencodeHint"]
]
}
],
[
3,
6,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
],
[
4,
7,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
],
[
5,
8,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
],
[
6,
9,
0,
{
"end_col": 0,
"hl_group": "OpencodeDiffAdd",
"right_gravity": true,
"virt_text_pos": "overlay",
"priority": 5000,
"ns_id": 3,
"end_row": 10,
"end_right_gravity": false,
"virt_text_hide": false,
"virt_text": [
["1", "OpencodeDiffAddGutter"],
["+", "OpencodeDiffAddGutter"],
[" ", "OpencodeDiffAddGutter"]
],
"virt_text_repeat_linebreak": false,
"hl_eol": true
}
],
[
7,
9,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
],
[
8,
10,
0,
{
"end_col": 0,
"hl_group": "OpencodeDiffAdd",
"right_gravity": true,
"virt_text_pos": "overlay",
"priority": 5000,
"ns_id": 3,
"end_row": 11,
"end_right_gravity": false,
"virt_text_hide": false,
"virt_text": [
["2", "OpencodeDiffAddGutter"],
["+", "OpencodeDiffAddGutter"],
[" ", "OpencodeDiffAddGutter"]
],
"virt_text_repeat_linebreak": false,
"hl_eol": true
}
],
[
9,
10,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
],
[
10,
11,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
],
[
11,
12,
0,
{
"ns_id": 3,
"virt_text_hide": false,
"priority": 4096,
"right_gravity": true,
"virt_text_pos": "win_col",
"virt_text_win_col": -1,
"virt_text_repeat_linebreak": true,
"virt_text": [["▌", "OpencodeToolBorder"]]
}
]
],
"lines": [
"----",
"",
"",
"----",
"",
"",
"** apply patch** `src/app/features/auth/__tests__/LoginForm.test.tsx` 4s",
"",
"`````tsx",
" import React from 'react'",
" // minimal diff for testing",
"",
"`````",
"",
""
],
"timestamp": 1772803135
}
Loading