vim.opt.rtp:prepend(".")
local t = require("test.runner")
local lsp = require("test.lsp")
local b = require("test.buf")
local buf = lsp.open_and_attach("test_files/sample.pl")
t.test("documentSymbol returns packages and classes", function()
local N = "documentSymbol returns packages and classes"
local names = lsp.symbol_names(buf)
if not t.ok(N, #names > 0, "no symbols returned") then return end
local ok = t.contains(N, names, "Calculator", "symbols")
ok = t.contains(N, names, "Point", "symbols") and ok
if ok then t.pass(N) end
end)
t.test("goto-def: $calc->add jumps to sub add", function()
local N = "goto-def: $calc->add jumps to sub add"
local line, col = b.find_pos(buf, "->add(2, 3)")
if not t.ok(N, line, "couldn't find '->add(2, 3)'") then return end
local def = lsp.def_line(buf, line, col + 2)
local expected = b.find_line(buf, "^sub add ")
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("goto-def: $p->magnitude jumps to method", function()
local N = "goto-def: $p->magnitude jumps to method"
local line, col = b.find_pos(buf, "$p->magnitude()")
if not t.ok(N, line, "couldn't find '$p->magnitude()'") then return end
local def = lsp.def_line(buf, line, col + 4)
local expected = b.find_line(buf, "method magnitude")
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("goto-def: $p->x jumps to field reader (via inserted line)", function()
local N = "goto-def: $p->x jumps to field reader"
local insert_at = b.append(buf, { "$p->x;" })
vim.wait(500)
local line, col = b.find_pos(buf, "$p->x;")
if not t.ok(N, line, "couldn't find inserted '$p->x;'") then return end
local def = lsp.def_line(buf, line, col + 4)
local expected = b.find_line(buf, "field $x :param :reader")
b.remove(buf, insert_at, insert_at + 1)
vim.wait(200)
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("completion: $calc-> returns Calculator methods", function()
local N = "completion: $calc-> returns Calculator methods"
local line, col = b.find_pos(buf, "$calc->add(2, 3)")
if not t.ok(N, line, "couldn't find '$calc->add'") then return end
local labels = lsp.completion_labels(buf, line, col + 7)
local ok = t.contains(N, labels, "add", "completions")
ok = t.contains(N, labels, "subtract", "completions") and ok
ok = t.contains(N, labels, "get_history", "completions") and ok
if ok then t.pass(N) end
end)
t.test("completion: $p-> returns Point methods", function()
local N = "completion: $p-> returns Point methods"
local line, col = b.find_pos(buf, "$p->magnitude()")
if not t.ok(N, line, "couldn't find '$p->magnitude()'") then return end
local labels = lsp.completion_labels(buf, line, col + 4)
local ok = t.contains(N, labels, "magnitude", "completions")
ok = t.contains(N, labels, "to_string", "completions") and ok
ok = t.contains(N, labels, "x", "completions") and ok
ok = t.contains(N, labels, "new", "completions") and ok
if ok then t.pass(N) end
end)
t.test("completion: $self-> inside method returns sibling methods", function()
local N = "completion: $self-> inside method returns sibling methods"
local line, col = b.find_pos(buf, "$self->magnitude()")
if not t.ok(N, line, "couldn't find '$self->magnitude()' in file") then return end
col = col + 7 local labels = lsp.completion_labels(buf, line, col)
local ok = t.contains(N, labels, "magnitude", "completions")
ok = t.contains(N, labels, "to_string", "completions") and ok
ok = t.contains(N, labels, "x", "completions") and ok
if ok then t.pass(N) end
end)
t.test("goto-def: $self->magnitude inside method jumps to method", function()
local N = "goto-def: $self->magnitude inside method"
local line, col = b.find_pos(buf, "$self->magnitude()")
if not t.ok(N, line, "couldn't find '$self->magnitude()'") then return end
col = col + 7 local def = lsp.def_line(buf, line, col)
local expected = b.find_line(buf, "^ method magnitude")
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("hover: sub add shows signature", function()
local N = "hover: sub add shows signature"
local line = b.find_line(buf, "^sub add ")
if not t.ok(N, line, "couldn't find 'sub add'") then return end
local text = lsp.hover_text(buf, line, 5)
if not t.ok(N, text, "no hover result") then return end
if t.ok(N, type(text) == "string" and text:find("add"), "hover doesn't mention 'add'") then
t.pass(N)
end
end)
t.test("hover: $sum shows Numeric type from method return", function()
local N = "hover: $sum shows Numeric type from method return"
local line, col = b.find_pos(buf, "my $sum = $calc->add")
if not t.ok(N, line, "couldn't find '$sum' line") then return end
local text = lsp.hover_text(buf, line, col + 4) if not t.ok(N, text, "no hover result") then return end
if t.ok(N, type(text) == "string" and text:find("Numeric"), "hover should show Numeric type, got: " .. text) then
t.pass(N)
end
end)
t.test("references: $pi finds declaration and usage", function()
local N = "references: $pi finds declaration and usage"
local decl = b.find_line(buf, "my $pi = ")
if not t.ok(N, decl, "couldn't find '$pi' declaration") then return end
local refs = lsp.reference_lines(buf, decl, 4)
if not t.ok(N, #refs >= 2, string.format("expected >=2 refs, got %d", #refs)) then return end
local usage = b.find_line(buf, "%$pi %* %$radius")
if not t.ok(N, usage, "couldn't find $pi usage line") then return end
if t.contains(N, refs, usage, "ref lines") then t.pass(N) end
end)
t.test("completion: $db_config-> offers hash keys from return type", function()
local N = "completion: $db_config-> offers hash keys from return type"
local line, col = b.find_pos(buf, "$db_config->{host}")
if not t.ok(N, line, "couldn't find '$db_config->{host}'") then return end
local labels = lsp.completion_labels(buf, line, col + 13) local ok = t.contains(N, labels, "host", "completions")
ok = t.contains(N, labels, "port", "completions") and ok
ok = t.contains(N, labels, "name", "completions") and ok
if ok then t.pass(N) end
end)
t.test("goto-def: chained $calc->get_self()->add resolves to sub add", function()
local N = "goto-def: chained $calc->get_self()->add resolves to sub add"
local line, col = b.find_pos(buf, "->get_self()->add(1, 2)")
if not t.ok(N, line, "couldn't find chained call") then return end
local def = lsp.def_line(buf, line, col + 14)
local expected = b.find_line(buf, "^sub add ")
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("goto-def: $db_config->{host} jumps to key in get_config return", function()
local N = "goto-def: $db_config->{host} jumps to key in get_config return"
local line, col = b.find_pos(buf, "$db_config->{host}")
if not t.ok(N, line, "couldn't find '$db_config->{host}'") then return end
local def = lsp.def_line(buf, line, col + 13)
local expected = b.find_line(buf, "host => \"localhost\"")
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("goto-def: $calc->get_self->get_config->{host} jumps to return hash key", function()
local N = "goto-def: chained get_config->{host}"
local line, col = b.find_pos(buf, "$calc->get_self->get_config->{host}")
if not t.ok(N, line, "couldn't find chained hash access") then return end
local def = lsp.def_line(buf, line, col + 30)
local expected = b.find_line(buf, "host => \"localhost\"")
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("completion: $calc->get_self->get_config->{ offers hash keys", function()
local N = "completion: $calc->get_self->get_config->{ offers hash keys"
local line, col = b.find_pos(buf, "$calc->get_self->get_config->{host}")
if not t.ok(N, line, "couldn't find chained hash access") then return end
local labels = lsp.completion_labels(buf, line, col + 31)
local ok = t.contains(N, labels, "host", "completions")
ok = t.contains(N, labels, "port", "completions") and ok
ok = t.contains(N, labels, "name", "completions") and ok
if ok then t.pass(N) end
end)
t.test("goto-def: x in Point->new(x => 3) jumps to field $x :param", function()
local N = "goto-def: x in Point->new(x => 3) jumps to field $x :param"
local line, col = b.find_pos(buf, "Point->new(x => 3")
if not t.ok(N, line, "couldn't find 'Point->new(x => 3'") then return end
local def = lsp.def_line(buf, line, col + 11)
local expected = b.find_line(buf, "field $x :param :reader")
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("goto-def: verbose in Calculator->new(verbose => 1) jumps to bless hash key", function()
local N = "goto-def: verbose in Calculator->new(verbose => 1) jumps to bless hash key"
local line, col = b.find_pos(buf, "Calculator->new(verbose")
if not t.ok(N, line, "couldn't find 'Calculator->new(verbose'") then return end
local def = lsp.def_line(buf, line, col + 16)
local expected = b.find_line(buf, "verbose => $args{verbose}")
if not t.ok(N, def, "no definition result") then return end
if t.eq(N, expected, def, "definition line") then t.pass(N) end
end)
t.test("inlay hint: $calc shows Calculator type", function()
local N = "inlay hint: $calc shows Calculator type"
local line, _ = b.find_pos(buf, "my $calc = Calculator->new")
if not t.ok(N, line, "couldn't find '$calc' declaration") then return end
local hints = lsp.inlay_hints(buf, line, line)
if not t.ok(N, #hints > 0, "no inlay hints returned") then return end
local found = false
for _, h in ipairs(hints) do
local label = type(h.label) == "string" and h.label or (h.label[1] and h.label[1].value or "")
if label:find("Calculator") then found = true; break end
end
if t.ok(N, found, "no hint mentioning Calculator") then t.pass(N) end
end)
t.test("inlay hint: $p shows Point type", function()
local N = "inlay hint: $p shows Point type"
local line, _ = b.find_pos(buf, "my $p = Point->new")
if not t.ok(N, line, "couldn't find '$p' declaration") then return end
local hints = lsp.inlay_hints(buf, line, line)
if not t.ok(N, #hints > 0, "no inlay hints returned") then return end
local found = false
for _, h in ipairs(hints) do
local label = type(h.label) == "string" and h.label or (h.label[1] and h.label[1].value or "")
if label:find("Point") then found = true; break end
end
if t.ok(N, found, "no hint mentioning Point") then t.pass(N) end
end)
t.test("inlay hint: $db_config shows HashRef type", function()
local N = "inlay hint: $db_config shows HashRef type"
local line, _ = b.find_pos(buf, "my $db_config = get_config")
if not t.ok(N, line, "couldn't find '$db_config' declaration") then return end
local hints = lsp.inlay_hints(buf, line, line)
if not t.ok(N, #hints > 0, "no inlay hints returned") then return end
local found = false
for _, h in ipairs(hints) do
local label = type(h.label) == "string" and h.label or (h.label[1] and h.label[1].value or "")
if label:find("HashRef") then found = true; break end
end
if t.ok(N, found, "no hint mentioning HashRef") then t.pass(N) end
end)
t.test("inlay hint: sub get_config shows → HashRef", function()
local N = "inlay hint: sub get_config shows → HashRef"
local line = b.find_line(buf, "^sub get_config")
if not t.ok(N, line, "couldn't find 'sub get_config'") then return end
local hints = lsp.inlay_hints(buf, line, line)
if not t.ok(N, #hints > 0, "no inlay hints returned") then return end
local found = false
for _, h in ipairs(hints) do
local label = type(h.label) == "string" and h.label or (h.label[1] and h.label[1].value or "")
if label:find("HashRef") then found = true; break end
end
if t.ok(N, found, "no return type hint for get_config") then t.pass(N) end
end)
t.test("completion detail: $calc->add shows return type", function()
local N = "completion detail: $calc->add shows return type"
local line, col = b.find_pos(buf, "$calc->add(2, 3)")
if not t.ok(N, line, "couldn't find '$calc->add'") then return end
local items = lsp.completion_items(buf, line, col + 7)
local found = false
for _, item in ipairs(items) do
if item.label == "add" and item.detail and item.detail:find("Numeric") then
found = true; break
end
end
if t.ok(N, found, "no 'add' completion with Numeric detail") then t.pass(N) end
end)
t.test("completion detail: $calc->get_self shows return type", function()
local N = "completion detail: $calc->get_self shows return type"
local line, col = b.find_pos(buf, "$calc->get_self()->add")
if not t.ok(N, line, "couldn't find chained call") then return end
local items = lsp.completion_items(buf, line, col + 7)
local found = false
for _, item in ipairs(items) do
if item.label == "get_self" and item.detail and item.detail:find("Calculator") then
found = true; break
end
end
if t.ok(N, found, "no 'get_self' completion with Calculator detail") then t.pass(N) end
end)
t.test("rename: $pi → $tau updates all occurrences", function()
local N = "rename: $pi → $tau updates all occurrences"
local line, col = b.find_pos(buf, "my $pi = 3.14159")
if not t.ok(N, line, "couldn't find '$pi' declaration") then return end
local edit = lsp.rename(buf, line, col + 3, "tau")
if not t.ok(N, edit, "rename returned no edit") then return end
lsp.apply_workspace_edit(edit)
b.invalidate()
local lines = b.get_lines(buf)
local found_old = false
local found_new = false
for _, l in ipairs(lines) do
if l:find("$pi", 1, true) and not l:find("$pid", 1, true) then found_old = true end
if l:find("$tau", 1, true) then found_new = true end
end
vim.cmd("silent undo")
b.invalidate()
vim.wait(500)
if not t.ok(N, not found_old, "old name $pi still found after rename") then return end
if t.ok(N, found_new, "new name $tau should appear after rename") then t.pass(N) end
end)
t.test("rename: host → hostname from deref access site", function()
local N = "rename: host → hostname from deref access site"
local line, col = b.find_pos(buf, "$db_config->{host}")
if not t.ok(N, line, "couldn't find '$db_config->{host}'") then return end
local edit = lsp.rename(buf, line, col + 13, "hostname")
if not t.ok(N, edit, "rename returned no edit") then return end
if not t.ok(N, edit.changes, "rename has no changes") then return end
local total = 0
for _, edits in pairs(edit.changes) do
total = total + #edits
end
if t.ok(N, total >= 2, string.format("should have ≥2 edits (def + access), got %d", total)) then
t.pass(N)
end
end)
lsp.assert_no_diagnostics(t, buf)
t.finish()