Skip to content

Commit 3fcd62e

Browse files
Copilotolimorris
andcommitted
Refactor UI formatters to use centralized spacing management
Co-authored-by: olimorris <[email protected]>
1 parent 33cf22e commit 3fcd62e

File tree

6 files changed

+283
-25
lines changed

6 files changed

+283
-25
lines changed

lua/codecompanion/strategies/chat/ui/builder.lua

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local config = require("codecompanion.config")
33
local Reasoning = require("codecompanion.strategies.chat.ui.formatters.reasoning")
44
local Standard = require("codecompanion.strategies.chat.ui.formatters.standard")
55
local Tools = require("codecompanion.strategies.chat.ui.formatters.tools")
6+
local Spacing = require("codecompanion.strategies.chat.ui.spacing")
67

78
local api = vim.api
89

@@ -143,17 +144,22 @@ function Builder:_should_add_header(data, opts, state)
143144
return (data.role and data.role ~= state.last_role) or (opts and opts.force_role)
144145
end
145146

146-
---Add appropriate spacing before header
147+
---Add appropriate spacing before header using centralized spacing manager
147148
---@param lines table
148149
---@param state table
149150
---@return nil
150151
function Builder:_add_header_spacing(lines, state)
151-
if state.last_type == self.chat.MESSAGE_TYPES.TOOL_MESSAGE then
152-
table.insert(lines, "")
153-
else
154-
table.insert(lines, "")
155-
table.insert(lines, "")
156-
end
152+
local spacing_context = {
153+
is_new_role = true,
154+
is_new_section = false,
155+
previous_type = state.last_type,
156+
current_type = nil, -- Headers are for roles, not specific message types
157+
has_reasoning_transition = false,
158+
is_reasoning_start = false,
159+
}
160+
161+
local spacing_lines = Spacing.get_header_spacing(spacing_context, self.chat.MESSAGE_TYPES)
162+
vim.list_extend(lines, spacing_lines)
157163
end
158164

159165
---Get the appropriate formatter

lua/codecompanion/strategies/chat/ui/formatters/reasoning.lua

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local BaseFormatter = require("codecompanion.strategies.chat.ui.formatters.base")
2+
local Spacing = require("codecompanion.strategies.chat.ui.spacing")
23

34
---@class CodeCompanion.Chat.UI.Formatters.Reasoning : CodeCompanion.Chat.UI.Formatters.Base
45
local Reasoning = setmetatable({}, { __index = BaseFormatter })
@@ -15,14 +16,24 @@ end
1516
function Reasoning:format(message, opts, state)
1617
local lines = {}
1718

18-
-- Use rich state methods
19+
-- Build spacing context
20+
local spacing_context = {
21+
is_new_role = false, -- Reasoning is always under LLM role
22+
is_new_section = state.is_new_section,
23+
previous_type = state.last_type,
24+
current_type = self:get_type(),
25+
has_reasoning_transition = false, -- We're starting reasoning, not transitioning from it
26+
is_reasoning_start = not state.has_reasoning_output,
27+
}
28+
29+
-- Add reasoning header if this is the start of reasoning output
1930
if not state.has_reasoning_output then
2031
table.insert(lines, "### Reasoning")
2132
table.insert(lines, "")
2233
state:mark_reasoning_started()
2334
end
2435

25-
-- Add reasoning content
36+
-- Add reasoning content directly (no additional spacing needed)
2637
for _, line in ipairs(vim.split(message.content, "\n", { plain = true, trimempty = false })) do
2738
table.insert(lines, line)
2839
end
@@ -31,3 +42,6 @@ function Reasoning:format(message, opts, state)
3142
end
3243

3344
return Reasoning
45+
end
46+
47+
return Reasoning

lua/codecompanion/strategies/chat/ui/formatters/standard.lua

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local BaseFormatter = require("codecompanion.strategies.chat.ui.formatters.base")
2+
local Spacing = require("codecompanion.strategies.chat.ui.spacing")
23

34
---@class CodeCompanion.Chat.UI.Formatters.Standard : CodeCompanion.Chat.UI.Formatters.Base
45
local Standard = setmetatable({}, { __index = BaseFormatter })
@@ -16,16 +17,26 @@ end
1617
function Standard:format(message, opts, state)
1718
local lines = {}
1819

19-
-- Handle transition from reasoning to response using rich state
20+
-- Build spacing context
21+
local spacing_context = {
22+
is_new_role = false, -- Standard messages are typically under LLM role
23+
is_new_section = state.is_new_section,
24+
previous_type = state.last_type,
25+
current_type = self:get_type(opts),
26+
has_reasoning_transition = state.has_reasoning_output,
27+
is_reasoning_start = false,
28+
}
29+
30+
-- Handle transition from reasoning to response
2031
if state.has_reasoning_output then
2132
state:mark_reasoning_complete()
22-
table.insert(lines, "")
23-
table.insert(lines, "")
33+
local transition_spacing = Spacing.get_pre_content_spacing(spacing_context, self.chat.MESSAGE_TYPES)
34+
vim.list_extend(lines, transition_spacing)
2435
table.insert(lines, "### Response")
2536
table.insert(lines, "")
2637
end
2738

28-
-- Add content
39+
-- Add content directly (spacing is handled at message boundary level)
2940
for _, line in ipairs(vim.split(message.content, "\n", { plain = true, trimempty = false })) do
3041
table.insert(lines, line)
3142
end
@@ -34,3 +45,6 @@ function Standard:format(message, opts, state)
3445
end
3546

3647
return Standard
48+
end
49+
50+
return Standard

lua/codecompanion/strategies/chat/ui/formatters/tools.lua

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local config = require("codecompanion.config")
22

33
local BaseFormatter = require("codecompanion.strategies.chat.ui.formatters.base")
4+
local Spacing = require("codecompanion.strategies.chat.ui.spacing")
45

56
---@class CodeCompanion.Chat.UI.Formatters.Tools : CodeCompanion.Chat.UI.Formatters.Base
67
local Tools = setmetatable({}, { __index = BaseFormatter })
@@ -17,37 +18,51 @@ end
1718
function Tools:format(message, opts, state)
1819
local lines = {}
1920

21+
-- Build spacing context for consistent spacing management
22+
local spacing_context = {
23+
is_new_role = false, -- Tools are always under LLM role
24+
is_new_section = state.is_new_section,
25+
previous_type = state.last_type,
26+
current_type = self:get_type(),
27+
has_reasoning_transition = state.has_reasoning_output,
28+
is_reasoning_start = false,
29+
}
30+
31+
-- Handle reasoning to response transition
2032
if state.has_reasoning_output then
2133
state:mark_reasoning_complete()
22-
table.insert(lines, "")
23-
table.insert(lines, "")
34+
local transition_spacing = Spacing.get_pre_content_spacing(spacing_context, self.chat.MESSAGE_TYPES)
35+
vim.list_extend(lines, transition_spacing)
2436
table.insert(lines, "### Response")
2537
end
2638

27-
if state.is_new_section then
28-
table.insert(lines, "")
29-
end
30-
if state.last_type == self.chat.MESSAGE_TYPES.LLM_MESSAGE then
31-
table.insert(lines, "")
39+
-- Get pre-content spacing (but not for reasoning transitions as they're handled above)
40+
if not state.has_reasoning_output then
41+
local pre_spacing = Spacing.get_pre_content_spacing(spacing_context, self.chat.MESSAGE_TYPES)
42+
vim.list_extend(lines, pre_spacing)
3243
end
3344

3445
local content_start = #lines + 1
3546
local content = message.content or ""
3647
for _, line in ipairs(vim.split(content, "\n", { plain = true, trimempty = false })) do
3748
table.insert(lines, line)
3849
end
39-
table.insert(lines, "")
50+
51+
-- Get post-content spacing
52+
local post_spacing = Spacing.get_post_content_spacing(spacing_context, self.chat.MESSAGE_TYPES)
53+
vim.list_extend(lines, post_spacing)
4054

4155
if not config.strategies.chat.tools.opts.folds.enabled then
4256
return lines, nil
4357
end
4458

45-
-- Folds can only work up to the penultimate line in the buffer, so an extra
46-
-- line has been added as a result. But, we don't want to fold this line
47-
local content_end = #lines - 1
59+
-- Folds can only work up to the penultimate line in the buffer, so we need
60+
-- to account for the trailing spacing in fold calculations
61+
local content_lines = vim.split(content, "\n", { plain = true, trimempty = false })
62+
local content_end = content_start + #content_lines - 1
4863

4964
local fold_info = nil
50-
if content_end >= content_start then
65+
if content_end >= content_start and #content_lines > 1 then
5166
fold_info = {
5267
start_offset = content_start - 1, -- 0-based index
5368
end_offset = content_end - 1, -- 0-based index
@@ -59,3 +74,6 @@ function Tools:format(message, opts, state)
5974
end
6075

6176
return Tools
77+
end
78+
79+
return Tools
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---Centralized spacing management for chat UI
2+
---Provides consistent spacing rules across all formatters
3+
4+
---@class CodeCompanion.Chat.UI.Spacing
5+
local Spacing = {}
6+
7+
---@class CodeCompanion.Chat.UI.SpacingContext
8+
---@field is_new_role boolean Whether this is a new role (triggers header)
9+
---@field is_new_section boolean Whether this is a new section under same role
10+
---@field previous_type string|nil The type of the previous message
11+
---@field current_type string The type of the current message
12+
---@field has_reasoning_transition boolean Whether transitioning from reasoning to response
13+
---@field is_reasoning_start boolean Whether starting reasoning output
14+
15+
---Determine spacing needed before content
16+
---@param context CodeCompanion.Chat.UI.SpacingContext
17+
---@param message_types table Available message type constants
18+
---@return table lines Array of spacing lines (empty strings)
19+
function Spacing.get_pre_content_spacing(context, message_types)
20+
local lines = {}
21+
22+
-- Handle reasoning transitions first (highest priority)
23+
if context.has_reasoning_transition then
24+
table.insert(lines, "")
25+
table.insert(lines, "")
26+
return lines
27+
end
28+
29+
-- Handle new sections (when type changes but role stays the same)
30+
if context.is_new_section then
31+
-- Less spacing for section transitions within same role
32+
if context.previous_type == message_types.LLM_MESSAGE
33+
and context.current_type == message_types.TOOL_MESSAGE then
34+
table.insert(lines, "")
35+
elseif context.previous_type == message_types.TOOL_MESSAGE
36+
and context.current_type == message_types.LLM_MESSAGE then
37+
table.insert(lines, "")
38+
end
39+
return lines
40+
end
41+
42+
-- For reasoning start, no extra spacing needed as header handles it
43+
if context.is_reasoning_start then
44+
return lines
45+
end
46+
47+
return lines
48+
end
49+
50+
---Determine spacing needed after content
51+
---@param context CodeCompanion.Chat.UI.SpacingContext
52+
---@param message_types table Available message type constants
53+
---@return table lines Array of spacing lines (empty strings)
54+
function Spacing.get_post_content_spacing(context, message_types)
55+
local lines = {}
56+
57+
-- Most content doesn't need trailing spacing
58+
-- The builder will handle spacing between messages
59+
60+
-- Only tool output might need a trailing line for folding purposes
61+
if context.current_type == message_types.TOOL_MESSAGE then
62+
table.insert(lines, "")
63+
end
64+
65+
return lines
66+
end
67+
68+
---Determine spacing needed before headers
69+
---@param context CodeCompanion.Chat.UI.SpacingContext
70+
---@param message_types table Available message type constants
71+
---@return table lines Array of spacing lines (empty strings)
72+
function Spacing.get_header_spacing(context, message_types)
73+
local lines = {}
74+
75+
-- Less aggressive spacing before headers
76+
if context.previous_type == message_types.TOOL_MESSAGE then
77+
-- Single line after tool messages
78+
table.insert(lines, "")
79+
elseif context.previous_type then
80+
-- Double line for other transitions (standard)
81+
table.insert(lines, "")
82+
table.insert(lines, "")
83+
end
84+
85+
return lines
86+
end
87+
88+
return Spacing

0 commit comments

Comments
 (0)