Skip to content

Commit 9880a74

Browse files
committed
feat(types): add lua language server type definitions and improve test assertions
Change-Id: I8ce4c0a4950096ff255aa338b6f5588b7c8f6eb9 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 725400e commit 9880a74

File tree

10 files changed

+251
-94
lines changed

10 files changed

+251
-94
lines changed

.luarc.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
3-
"Lua.diagnostics.globals": ["vim"],
3+
"Lua.diagnostics.globals": [
4+
"vim",
5+
"setup",
6+
"teardown",
7+
"before_each",
8+
"after_each",
9+
"match",
10+
"assert"
11+
],
412
"Lua.runtime.version": "LuaJIT",
513
"Lua.workspace.checkThirdParty": false,
6-
"Lua.workspace.library": ["${3rd}/luv/library"],
14+
"Lua.workspace.library": ["${3rd}/luv/library", "lua/claudecode/meta"],
715
"Lua.workspace.maxPreload": 2000,
816
"Lua.workspace.preloadFileSize": 1000,
917
"Lua.telemetry.enable": false,
10-
"diagnostics.disable": ["lowercase-global"]
18+
"Lua.diagnostics.disable": ["lowercase-global"]
1119
}

lua/claudecode/init.lua

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ M.state = {
6060
initialized = false,
6161
}
6262

63+
---@alias ClaudeCode.TerminalOpts { \
64+
--- split_side?: "left"|"right", \
65+
--- split_width_percentage?: number, \
66+
--- provider?: "snacks"|"native", \
67+
--- show_native_term_exit_tip?: boolean }
68+
---
69+
---@alias ClaudeCode.SetupOpts { \
70+
--- terminal?: ClaudeCode.TerminalOpts }
71+
---
6372
--- Set up the plugin with user configuration
64-
---@param opts table|nil Optional configuration table to override defaults.
65-
---@field opts.terminal table|nil Configuration for the terminal module.
66-
---@field opts.terminal.split_side string|nil 'left' or 'right'.
67-
---@field opts.terminal.split_width_percentage number|nil Percentage of screen width (0.0 to 1.0).
68-
---@field opts.terminal.provider string|nil 'snacks' or 'native'.
69-
---@field opts.terminal.show_native_term_exit_tip boolean|nil Show tip for exiting native terminal (default: true).
73+
---@param opts ClaudeCode.SetupOpts|nil Optional configuration table to override defaults.
7074
---@return table The plugin module
7175
function M.setup(opts)
7276
opts = opts or {}
@@ -134,7 +138,7 @@ function M.start(show_startup_notification)
134138
end
135139

136140
M.state.server = server
137-
M.state.port = result
141+
M.state.port = tonumber(result)
138142

139143
local lockfile = require("claudecode.lockfile")
140144
local lock_success, lock_result = lockfile.create(M.state.port)

lua/claudecode/meta/vim.lua

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---@meta vim_api_definitions
2+
-- This file provides type definitions for parts of the Neovim API
3+
-- to help the Lua language server (LuaLS) with diagnostics.
4+
5+
---@class vim_log_levels
6+
---@field NONE number
7+
---@field ERROR number
8+
---@field WARN number
9+
---@field INFO number
10+
---@field DEBUG number
11+
---@field TRACE number
12+
13+
---@class vim_log
14+
---@field levels vim_log_levels
15+
16+
---@class vim_notify_opts
17+
---@field title string|nil
18+
---@field icon string|nil
19+
---@field on_open fun(winid: number)|nil
20+
---@field on_close fun()|nil
21+
---@field timeout number|nil
22+
---@field keep fun()|nil
23+
---@field plugin string|nil
24+
---@field hide_from_history boolean|nil
25+
---@field once boolean|nil
26+
---@field on_close_timeout number|nil
27+
28+
---@class vim_global_api
29+
---@field notify fun(msg: string | string[], level?: number, opts?: vim_notify_opts):nil
30+
---@field log vim_log
31+
---@field _last_echo table[]? # table of tables, e.g. { {"message", "HighlightGroup"} }
32+
---@field _last_error string?
33+
-- Add other vim object definitions here if they cause linting issues
34+
-- e.g. vim.fn, vim.api, vim.loop, vim.deepcopy, etc.
35+
36+
---@class SpyCall
37+
---@field vals table[] table of arguments passed to the call
38+
---@field self any the 'self' object for the call if it was a method
39+
40+
---@class SpyInformation
41+
---@field calls SpyCall[] A list of calls made to the spy.
42+
---@field call_count number The number of times the spy has been called.
43+
-- Add other spy properties if needed e.g. returned, threw
44+
45+
---@class SpyAsserts
46+
---@field was_called fun(self: SpyAsserts, count?: number):boolean
47+
---@field was_called_with fun(self: SpyAsserts, ...):boolean
48+
---@field was_not_called fun(self: SpyAsserts):boolean
49+
-- Add other spy asserts if needed
50+
51+
---@class SpyableFunction : function
52+
---@field __call fun(self: SpyableFunction, ...):any
53+
---@field spy fun(self: SpyableFunction):SpyAsserts Returns an assertion object for the spy.
54+
---@field calls SpyInformation[]? Information about calls made to the spied function.
55+
-- Note: In some spy libraries, 'calls' might be directly on the spied function,
56+
-- or on an object returned by `spy()`. Adjust as per your spy library's specifics.
57+
-- For busted's default spy, `calls` is often directly on the spied function.
58+
59+
-- This section helps LuaLS understand that 'vim' is a global variable
60+
-- with the structure defined above. It's for type hinting only and
61+
-- does not execute or overwrite the actual 'vim' global provided by Neovim.
62+
---@type vim_global_api

lua/claudecode/server/mock.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ M.state = {
1818

1919
--- Find an available port in the given range
2020
---@param min number The minimum port number
21-
---@param max number The maximum port number
21+
---@param _max number The maximum port number
2222
---@return number port The selected port
2323
function M.find_available_port(min, _max)
2424
-- For mock implementation, just return the minimum port

tests/config_test.lua

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
-- Simple config module tests that don't rely on the vim API
22

33
-- Create minimal vim mock
4-
_G.vim = {
4+
_G.vim = { ---@type vim_global_api
55
deepcopy = function(t)
6+
-- Basic deepcopy for testing
67
local copy = {}
78
for k, v in pairs(t) do
89
if type(v) == "table" then
@@ -13,6 +14,17 @@ _G.vim = {
1314
end
1415
return copy
1516
end,
17+
notify = function(msg, level, opts) end,
18+
log = {
19+
levels = {
20+
NONE = 0,
21+
ERROR = 1,
22+
WARN = 2,
23+
INFO = 3,
24+
DEBUG = 4,
25+
TRACE = 5,
26+
},
27+
},
1628

1729
tbl_deep_extend = function(behavior, ...)
1830
local result = {}
@@ -45,13 +57,13 @@ describe("Config module", function()
4557
end)
4658

4759
it("should have default values", function()
48-
assert.is_table(config.defaults)
49-
assert.is_table(config.defaults.port_range)
50-
assert.is_number(config.defaults.port_range.min)
51-
assert.is_number(config.defaults.port_range.max)
52-
assert.is_boolean(config.defaults.auto_start)
53-
assert.is_string(config.defaults.log_level)
54-
assert.is_boolean(config.defaults.track_selection)
60+
assert(type(config.defaults) == "table")
61+
assert(type(config.defaults.port_range) == "table")
62+
assert(type(config.defaults.port_range.min) == "number")
63+
assert(type(config.defaults.port_range.max) == "number")
64+
assert(type(config.defaults.auto_start) == "boolean")
65+
assert(type(config.defaults.log_level) == "string")
66+
assert(type(config.defaults.track_selection) == "boolean")
5567
end)
5668

5769
it("should validate valid configuration", function()
@@ -67,7 +79,7 @@ describe("Config module", function()
6779
return config.validate(valid_config)
6880
end)
6981

70-
assert.is_true(success)
82+
assert(success == true)
7183
end)
7284

7385
it("should merge user config with defaults", function()
@@ -78,9 +90,9 @@ describe("Config module", function()
7890

7991
local merged_config = config.apply(user_config)
8092

81-
assert.is_true(merged_config.auto_start)
82-
assert.equals("debug", merged_config.log_level)
83-
assert.equals(config.defaults.port_range.min, merged_config.port_range.min)
84-
assert.equals(config.defaults.track_selection, merged_config.track_selection)
93+
assert(merged_config.auto_start == true)
94+
assert("debug" == merged_config.log_level)
95+
assert(config.defaults.port_range.min == merged_config.port_range.min)
96+
assert(config.defaults.track_selection == merged_config.track_selection)
8597
end)
8698
end)

tests/lockfile_test.lua

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
local real_vim = _G.vim
55
if not _G.vim then
66
-- Create a basic vim mock
7-
_G.vim = {
7+
_G.vim = { -- Removed ---@type vim_global_api annotation
88
fn = {
99
expand = function(path)
1010
return path:gsub("~", "/home/user")
@@ -22,6 +22,17 @@ if not _G.vim then
2222
return 1
2323
end,
2424
},
25+
notify = function(msg, level, opts) end,
26+
log = {
27+
levels = {
28+
NONE = 0,
29+
ERROR = 1,
30+
WARN = 2,
31+
INFO = 3,
32+
DEBUG = 4,
33+
TRACE = 5,
34+
},
35+
},
2536
json = {
2637
encode = function(_obj) -- Prefix unused param with underscore
2738
return '{"mocked":"json"}'
@@ -107,7 +118,7 @@ describe("Lockfile Module", function()
107118

108119
it("should include the current working directory", function()
109120
local folders = lockfile.get_workspace_folders()
110-
assert.equals("/mock/cwd", folders[1])
121+
assert("/mock/cwd" == folders[1])
111122
end)
112123

113124
it("should work with current Neovim API (get_clients)", function()
@@ -118,9 +129,9 @@ describe("Lockfile Module", function()
118129
local folders = lockfile.get_workspace_folders()
119130

120131
-- Verify results
121-
assert.equals(3, #folders) -- cwd + 2 workspace folders
122-
assert.equals("/mock/folder1", folders[2])
123-
assert.equals("/mock/folder2", folders[3])
132+
assert(3 == #folders) -- cwd + 2 workspace folders
133+
assert("/mock/folder1" == folders[2])
134+
assert("/mock/folder2" == folders[3])
124135
end)
125136

126137
it("should work with legacy Neovim API (get_active_clients)", function()
@@ -131,9 +142,9 @@ describe("Lockfile Module", function()
131142
local folders = lockfile.get_workspace_folders()
132143

133144
-- Verify results
134-
assert.equals(3, #folders) -- cwd + 2 workspace folders
135-
assert.equals("/mock/folder1", folders[2])
136-
assert.equals("/mock/folder2", folders[3])
145+
assert(3 == #folders) -- cwd + 2 workspace folders
146+
assert("/mock/folder1" == folders[2])
147+
assert("/mock/folder2" == folders[3])
137148
end)
138149

139150
it("should handle duplicate folder paths", function()
@@ -158,7 +169,7 @@ describe("Lockfile Module", function()
158169
local folders = lockfile.get_workspace_folders()
159170

160171
-- Verify results
161-
assert.equals(2, #folders) -- cwd + 1 unique workspace folder
172+
assert(2 == #folders) -- cwd + 1 unique workspace folder
162173
end)
163174
end)
164175
end)

tests/selection_test.lua

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,19 @@ if not _G.vim then
185185
}
186186
end,
187187
},
188+
189+
-- Added notify and log mocks
190+
notify = function(msg, level, opts) end,
191+
log = {
192+
levels = {
193+
NONE = 0,
194+
ERROR = 1,
195+
WARN = 2,
196+
INFO = 3,
197+
DEBUG = 4,
198+
TRACE = 5,
199+
},
200+
},
188201
}
189202

190203
-- Initialize with a test buffer
@@ -224,26 +237,26 @@ describe("Selection module", function()
224237
end)
225238

226239
it("should have the correct initial state", function()
227-
assert.is_table(selection.state)
228-
assert.is_nil(selection.state.latest_selection)
229-
assert.is_false(selection.state.tracking_enabled)
230-
assert.is_nil(selection.state.debounce_timer)
231-
assert.is_number(selection.state.debounce_ms)
240+
assert(type(selection.state) == "table")
241+
assert(selection.state.latest_selection == nil)
242+
assert(selection.state.tracking_enabled == false)
243+
assert(selection.state.debounce_timer == nil)
244+
assert(type(selection.state.debounce_ms) == "number")
232245
end)
233246

234247
it("should enable and disable tracking", function()
235248
-- Enable tracking
236249
selection.enable(mock_server)
237250

238-
assert.is_true(selection.state.tracking_enabled)
239-
assert.equals(mock_server, selection.server)
251+
assert(selection.state.tracking_enabled == true)
252+
assert(mock_server == selection.server)
240253

241254
-- Disable tracking
242255
selection.disable()
243256

244-
assert.is_false(selection.state.tracking_enabled)
245-
assert.is_nil(selection.server)
246-
assert.is_nil(selection.state.latest_selection)
257+
assert(selection.state.tracking_enabled == false)
258+
assert(selection.server == nil)
259+
assert(selection.state.latest_selection == nil)
247260
end)
248261

249262
it("should get cursor position in normal mode", function()
@@ -261,20 +274,20 @@ describe("Selection module", function()
261274
-- Restore original function
262275
_G.vim.api.nvim_win_get_cursor = old_win_get_cursor
263276

264-
assert.is_table(cursor_pos)
265-
assert.equals("", cursor_pos.text)
266-
assert.is_string(cursor_pos.filePath)
267-
assert.is_string(cursor_pos.fileUrl)
268-
assert.is_table(cursor_pos.selection)
269-
assert.is_table(cursor_pos.selection.start)
270-
assert.is_table(cursor_pos.selection["end"])
277+
assert(type(cursor_pos) == "table")
278+
assert("" == cursor_pos.text)
279+
assert(type(cursor_pos.filePath) == "string")
280+
assert(type(cursor_pos.fileUrl) == "string")
281+
assert(type(cursor_pos.selection) == "table")
282+
assert(type(cursor_pos.selection.start) == "table")
283+
assert(type(cursor_pos.selection["end"]) == "table")
271284

272285
-- Check positions - 0-based in selection, source is 1-based from nvim_win_get_cursor
273-
assert.equals(1, cursor_pos.selection.start.line) -- Should be 2-1=1
274-
assert.equals(3, cursor_pos.selection.start.character)
275-
assert.equals(1, cursor_pos.selection["end"].line)
276-
assert.equals(3, cursor_pos.selection["end"].character)
277-
assert.is_true(cursor_pos.selection.isEmpty)
286+
assert(1 == cursor_pos.selection.start.line) -- Should be 2-1=1
287+
assert(3 == cursor_pos.selection.start.character)
288+
assert(1 == cursor_pos.selection["end"].line)
289+
assert(3 == cursor_pos.selection["end"].character)
290+
assert(cursor_pos.selection.isEmpty == true)
278291
end)
279292

280293
it("should detect selection changes", function()
@@ -337,15 +350,15 @@ describe("Selection module", function()
337350
selection.state.latest_selection = old_selection
338351

339352
-- Test same selection
340-
assert.is_false(selection.has_selection_changed(new_selection_same))
353+
assert(selection.has_selection_changed(new_selection_same) == false)
341354

342355
-- Test different file
343-
assert.is_true(selection.has_selection_changed(new_selection_diff_file))
356+
assert(selection.has_selection_changed(new_selection_diff_file) == true)
344357

345358
-- Test different text
346-
assert.is_true(selection.has_selection_changed(new_selection_diff_text))
359+
assert(selection.has_selection_changed(new_selection_diff_text) == true)
347360

348361
-- Test different position
349-
assert.is_true(selection.has_selection_changed(new_selection_diff_pos))
362+
assert(selection.has_selection_changed(new_selection_diff_pos) == true)
350363
end)
351364
end)

0 commit comments

Comments
 (0)