-
Notifications
You must be signed in to change notification settings - Fork 187
Expand file tree
/
Copy pathconfig.lua
More file actions
223 lines (196 loc) · 8.96 KB
/
config.lua
File metadata and controls
223 lines (196 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
---@brief [[
--- Manages configuration for the Claude Code Neovim integration.
--- Provides default settings, validation, and application of user-defined configurations.
---@brief ]]
---@module 'claudecode.config'
local M = {}
---@type ClaudeCodeConfig
M.defaults = {
port_range = { min = 10000, max = 65535 },
auto_start = true,
terminal_cmd = nil,
env = {}, -- Custom environment variables for Claude terminal
log_level = "info",
track_selection = true,
-- When true, focus Claude terminal after a successful send while connected
focus_after_send = false,
visual_demotion_delay_ms = 50, -- Milliseconds to wait before demoting a visual selection
connection_wait_delay = 600, -- Milliseconds to wait after connection before sending queued @ mentions
connection_timeout = 10000, -- Maximum time to wait for Claude Code to connect (milliseconds)
queue_timeout = 5000, -- Maximum time to keep @ mentions in queue (milliseconds)
diff_opts = {
layout = "vertical",
open_in_new_tab = false, -- Open diff in a new tab (false = use current tab)
keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens (including floating terminals)
hide_terminal_in_new_tab = false, -- If true and opening in a new tab, do not show Claude terminal there
on_new_file_reject = "keep_empty", -- "keep_empty" leaves an empty buffer; "close_window" closes the placeholder split
},
models = {
{ name = "Claude Opus 4.1 (Latest)", value = "opus" },
{ name = "Claude Sonnet 4.5 (Latest)", value = "sonnet" },
{ name = "Opusplan: Claude Opus 4.1 (Latest) + Sonnet 4.5 (Latest)", value = "opusplan" },
{ name = "Claude Haiku 4.5 (Latest)", value = "haiku" },
},
terminal = nil, -- Will be lazy-loaded to avoid circular dependency
}
---Validates the provided configuration table.
---Throws an error if any validation fails.
---@param config table The configuration table to validate.
---@return boolean true if the configuration is valid.
function M.validate(config)
assert(
type(config.port_range) == "table"
and type(config.port_range.min) == "number"
and type(config.port_range.max) == "number"
and config.port_range.min > 0
and config.port_range.max <= 65535
and config.port_range.min <= config.port_range.max,
"Invalid port range"
)
assert(type(config.auto_start) == "boolean", "auto_start must be a boolean")
assert(config.terminal_cmd == nil or type(config.terminal_cmd) == "string", "terminal_cmd must be nil or a string")
-- Validate terminal config
assert(type(config.terminal) == "table", "terminal must be a table")
-- Validate provider_opts if present
if config.terminal.provider_opts then
assert(type(config.terminal.provider_opts) == "table", "terminal.provider_opts must be a table")
-- Validate external_terminal_cmd in provider_opts
if config.terminal.provider_opts.external_terminal_cmd then
local cmd_type = type(config.terminal.provider_opts.external_terminal_cmd)
assert(
cmd_type == "string" or cmd_type == "function",
"terminal.provider_opts.external_terminal_cmd must be a string or function"
)
-- Only validate %s placeholder for strings
if cmd_type == "string" and config.terminal.provider_opts.external_terminal_cmd ~= "" then
assert(
config.terminal.provider_opts.external_terminal_cmd:find("%%s"),
"terminal.provider_opts.external_terminal_cmd must contain '%s' placeholder for the Claude command"
)
end
end
end
local valid_log_levels = { "trace", "debug", "info", "warn", "error" }
local is_valid_log_level = false
for _, level in ipairs(valid_log_levels) do
if config.log_level == level then
is_valid_log_level = true
break
end
end
assert(is_valid_log_level, "log_level must be one of: " .. table.concat(valid_log_levels, ", "))
assert(type(config.track_selection) == "boolean", "track_selection must be a boolean")
-- Allow absence in direct validate() calls; apply() supplies default
if config.focus_after_send ~= nil then
assert(type(config.focus_after_send) == "boolean", "focus_after_send must be a boolean")
end
assert(
type(config.visual_demotion_delay_ms) == "number" and config.visual_demotion_delay_ms >= 0,
"visual_demotion_delay_ms must be a non-negative number"
)
assert(
type(config.connection_wait_delay) == "number" and config.connection_wait_delay >= 0,
"connection_wait_delay must be a non-negative number"
)
assert(
type(config.connection_timeout) == "number" and config.connection_timeout > 0,
"connection_timeout must be a positive number"
)
assert(type(config.queue_timeout) == "number" and config.queue_timeout > 0, "queue_timeout must be a positive number")
assert(type(config.diff_opts) == "table", "diff_opts must be a table")
-- New diff options (optional validation to allow backward compatibility)
if config.diff_opts.layout ~= nil then
assert(
config.diff_opts.layout == "vertical" or config.diff_opts.layout == "horizontal",
"diff_opts.layout must be 'vertical' or 'horizontal'"
)
end
if config.diff_opts.open_in_new_tab ~= nil then
assert(type(config.diff_opts.open_in_new_tab) == "boolean", "diff_opts.open_in_new_tab must be a boolean")
end
if config.diff_opts.keep_terminal_focus ~= nil then
assert(type(config.diff_opts.keep_terminal_focus) == "boolean", "diff_opts.keep_terminal_focus must be a boolean")
end
if config.diff_opts.hide_terminal_in_new_tab ~= nil then
assert(
type(config.diff_opts.hide_terminal_in_new_tab) == "boolean",
"diff_opts.hide_terminal_in_new_tab must be a boolean"
)
end
if config.diff_opts.on_new_file_reject ~= nil then
assert(
type(config.diff_opts.on_new_file_reject) == "string"
and (
config.diff_opts.on_new_file_reject == "keep_empty" or config.diff_opts.on_new_file_reject == "close_window"
),
"diff_opts.on_new_file_reject must be 'keep_empty' or 'close_window'"
)
end
-- Legacy diff options (accept if present to avoid breaking old configs)
if config.diff_opts.auto_close_on_accept ~= nil then
assert(type(config.diff_opts.auto_close_on_accept) == "boolean", "diff_opts.auto_close_on_accept must be a boolean")
end
if config.diff_opts.show_diff_stats ~= nil then
assert(type(config.diff_opts.show_diff_stats) == "boolean", "diff_opts.show_diff_stats must be a boolean")
end
if config.diff_opts.vertical_split ~= nil then
assert(type(config.diff_opts.vertical_split) == "boolean", "diff_opts.vertical_split must be a boolean")
end
if config.diff_opts.open_in_current_tab ~= nil then
assert(type(config.diff_opts.open_in_current_tab) == "boolean", "diff_opts.open_in_current_tab must be a boolean")
end
-- Validate env
assert(type(config.env) == "table", "env must be a table")
for key, value in pairs(config.env) do
assert(type(key) == "string", "env keys must be strings")
assert(type(value) == "string", "env values must be strings")
end
-- Validate models
assert(type(config.models) == "table", "models must be a table")
assert(#config.models > 0, "models must not be empty")
for i, model in ipairs(config.models) do
assert(type(model) == "table", "models[" .. i .. "] must be a table")
assert(type(model.name) == "string" and model.name ~= "", "models[" .. i .. "].name must be a non-empty string")
assert(type(model.value) == "string" and model.value ~= "", "models[" .. i .. "].value must be a non-empty string")
end
return true
end
---Applies user configuration on top of default settings and validates the result.
---@param user_config table|nil The user-provided configuration table.
---@return ClaudeCodeConfig config The final, validated configuration table.
function M.apply(user_config)
local config = vim.deepcopy(M.defaults)
-- Lazy-load terminal defaults to avoid circular dependency
if config.terminal == nil then
local terminal_ok, terminal_module = pcall(require, "claudecode.terminal")
if terminal_ok and terminal_module.defaults then
config.terminal = terminal_module.defaults
end
end
if user_config then
-- Use vim.tbl_deep_extend if available, otherwise simple merge
if vim.tbl_deep_extend then
config = vim.tbl_deep_extend("force", config, user_config)
else
-- Simple fallback for testing environment
for k, v in pairs(user_config) do
config[k] = v
end
end
end
-- Backward compatibility: map legacy diff options to new fields if provided
if config.diff_opts then
local d = config.diff_opts
-- Map vertical_split -> layout (legacy option takes precedence)
if type(d.vertical_split) == "boolean" then
d.layout = d.vertical_split and "vertical" or "horizontal"
end
-- Map open_in_current_tab -> open_in_new_tab (legacy option takes precedence)
if type(d.open_in_current_tab) == "boolean" then
d.open_in_new_tab = not d.open_in_current_tab
end
end
M.validate(config)
return config
end
return M