diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16cac9a..67646fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,12 +16,12 @@ jobs: - uses: actions/checkout@main - name: Install Lua - uses: leafo/gh-actions-lua@v8.0.0 + uses: leafo/gh-actions-lua@v10 with: luaVersion: ${{ matrix.luaVersion }} - name: Install LuaRocks - uses: leafo/gh-actions-luarocks@v4.0.0 + uses: leafo/gh-actions-luarocks@v4 - name: Build run: scripts/setup_local_luarocks.sh diff --git a/gen/teal_language_server/document.lua b/gen/teal_language_server/document.lua index e5d4c51..c0ba332 100644 --- a/gen/teal_language_server/document.lua +++ b/gen/teal_language_server/document.lua @@ -9,7 +9,7 @@ local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") local ltreesitter = require("ltreesitter") -local teal_parser = ltreesitter.load("./teal.so", "teal") +local teal_parser = ltreesitter.require("parser/teal", "teal") local tl = require("tl") @@ -397,7 +397,7 @@ function Document:type_information_for_tokens(tokens, y, x) return nil end -function Document:_parser_token(y, x) +function Document:_tree_sitter_token(y, x) local moved = self._tree_cursor:goto_first_child() local node = self._tree_cursor:current_node() @@ -487,11 +487,11 @@ function Document:_parser_token(y, x) if y == start_point.row and y == end_point.row then if x >= start_point.column and x <= end_point.column then - return self:_parser_token(y, x) + return self:_tree_sitter_token(y, x) end elseif y >= start_point.row and y <= end_point.row then - return self:_parser_token(y, x) + return self:_tree_sitter_token(y, x) end moved = self._tree_cursor:goto_next_sibling() @@ -499,9 +499,9 @@ function Document:_parser_token(y, x) end end -function Document:parser_token(y, x) +function Document:tree_sitter_token(y, x) self._tree_cursor:reset(self._tree:root()) - return self:_parser_token(y, x) + return self:_tree_sitter_token(y, x) end class.setup(Document, "Document", { diff --git a/gen/teal_language_server/misc_handlers.lua b/gen/teal_language_server/misc_handlers.lua index 5c6ffc4..1354f44 100644 --- a/gen/teal_language_server/misc_handlers.lua +++ b/gen/teal_language_server/misc_handlers.lua @@ -167,7 +167,7 @@ function MiscHandlers:_get_node_info(params, pos) end tracing.warning(_module_name, "Received request for completion at position: {}", { pos }) - local node_info = doc:parser_token(pos.line, pos.character) + local node_info = doc:tree_sitter_token(pos.line, pos.character) if node_info == nil then tracing.warning(_module_name, "Unable to retrieve node info from tree-sitter parser", {}) return nil diff --git a/spec/document_tree_sitter_spec.lua b/spec/document_tree_sitter_spec.lua new file mode 100644 index 0000000..f6dd2a3 --- /dev/null +++ b/spec/document_tree_sitter_spec.lua @@ -0,0 +1,355 @@ +local Document = require("teal_language_server.document") +local ServerState = require("teal_language_server.server_state") + +describe("tree_sitter_parser", function() + it("should analyze basic function defintions", function() + local content = [[local function a() end]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 2) + assert.same(node_info.type, "local") + assert.same(node_info.parent_type, "function_statement") + + node_info = doc:tree_sitter_token(0, 8) + assert.same(node_info.type, "function") + end) + + it("returns nil on empty char", function() + local content = [[local ]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 6) + assert.is_nil(node_info) + end) + + it("returns on empty content", function() + local content = [[]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 0) + assert.same(node_info.type, "program") + + -- and on chars that aren't there yet + local node_info = doc:tree_sitter_token(0, 6) + assert.same(node_info.type, "program") + end) + + it("identifies function calls and vars", function() + local content = [[local dir = require("pl.dir")]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 16) + assert.same(node_info.parent_type, "function_call") + + local node_info = doc:tree_sitter_token(0, 8) + assert.same(node_info.parent_type, "var") + + local node_info = doc:tree_sitter_token(0, 3) + assert.same(node_info.parent_type, "var_declaration") + end) + + it("should recognize when at a .", function() + local content = [[ +local dir = require("pl.dir") +dir. + ]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(1, 4) + assert.same(node_info.type, ".") + assert.same(node_info.parent_type, "ERROR") + assert.same(node_info.preceded_by, "dir") + end) + + it("should recognize when at a :", function() + local content = [[ +local t = "fruit" +t: + ]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(1, 2) + assert.same(node_info.type, ":") + assert.same(node_info.parent_type, "ERROR") + assert.same(node_info.preceded_by, "t") + end) + + it("should recognize a nested .", function() + local content = [[string.byte(t.,]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 14) + assert.same(node_info.type, ".") + assert.same(node_info.parent_type, "ERROR") + assert.same(node_info.preceded_by, "t") + + local node_info = doc:tree_sitter_token(0, 7) + assert.same(node_info.type, ".") + assert.same(node_info.parent_type, "index") + assert.same(node_info.preceded_by, "string") + end) + + it("should handle chained .", function() + local content = [[lsp.completion_context.]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 23) + assert.same(node_info.type, ".") + assert.same(node_info.parent_type, "ERROR") + assert.same(node_info.preceded_by, "lsp.completion_context") + + local node_info = doc:tree_sitter_token(0, 20) + assert.same(node_info.type, "identifier") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "lsp.completion_context") + assert.is_nil(node_info.preceded_by) + + local node_info = doc:tree_sitter_token(0, 4) + assert.same(node_info.type, ".") + assert.same(node_info.parent_type, "index") + assert.same(node_info.preceded_by, "lsp") + end) + + it("should handle a variable defintion", function() + local content = [[local fruit: string = "thing"]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 9) + assert.same(node_info.parent_type, "var") + assert.same(node_info.source, "fruit") + + local node_info = doc:tree_sitter_token(0, 16) + assert.same(node_info.parent_type, "simple_type") + assert.same(node_info.source, "string") + + local node_info = doc:tree_sitter_token(0, 26) + assert.same(node_info.parent_type, "string") + assert.same(node_info.source, "thing") + end) + + it("should handle a basic self function", function() + local content = [[ +function Point:move(dx: number, dy: number) + self.x = self.x + dx + self.y = self.y + dy +end + ]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 13) + assert.same(node_info.parent_type, "function_name") + assert.same(node_info.source, "Point") + + local node_info = doc:tree_sitter_token(0, 18) + assert.same(node_info.parent_type, "function_name") + assert.same(node_info.source, "move") + + local node_info = doc:tree_sitter_token(0, 34) + assert.same(node_info.parent_type, "arg") + assert.same(node_info.source, "dy") + + local node_info = doc:tree_sitter_token(2, 6) + assert.same(node_info.parent_type, "index") + assert.same(node_info.self_type, "Point") + end) + + it("", function() + local content = [[ +function Document:thing() +function fruit() +self._something:fruit +end +end + ]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(2, 10) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "_something") + assert.same(node_info.parent_type, "function_name") + assert.same(node_info.parent_source, "self._something:fruit") + assert.same(node_info.self_type, "Document") + + local node_info = doc:tree_sitter_token(2, 20) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "fruit") + assert.same(node_info.parent_type, "function_name") + assert.same(node_info.parent_source, "self._something:fruit") + assert.same(node_info.self_type, "Document") + + local node_info = doc:tree_sitter_token(2, 16) + assert.same(node_info.type, ":") + assert.same(node_info.source, ":") + assert.same(node_info.parent_type, "function_name") + assert.same(node_info.parent_source, "self._something:fruit") + assert.same(node_info.preceded_by, "_something") + assert.same(node_info.self_type, "Document") + end) + + it("should handle even more nested .'s", function() + local content = [[lsp.orange.depot.box]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 6) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "orange") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "lsp.orange") + + local node_info = doc:tree_sitter_token(0, 13) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "depot") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "lsp.orange.depot") + + local node_info = doc:tree_sitter_token(0, 17) + assert.same(node_info.type, ".") + assert.same(node_info.source, ".") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "lsp.orange.depot.box") + assert.same(node_info.preceded_by, "lsp.orange.depot") + + end) + + it("should handle partial method chains", function() + local content = [[string.byte(t:fruit():,]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 22) + assert.same(node_info.type, ":") + assert.same(node_info.source, ":") + assert.same(node_info.parent_type, "ERROR") + assert.same(node_info.parent_source, "string.byte(t:fruit():,") + assert.same(node_info.preceded_by, "t:fruit()") + end) + + it("should handle real code pulling out self", function() + local content = [[ +function MiscHandlers:initialize() + self:_add_handler("initialize", self._on_initialize) + self:_add_handler("initialized", self._on_initialized) + self:_add_handler("textDocument/didOpen", self._on_did_open) + self:_add_handler("textDocument/didClose", self._on_did_close) + self:_add_handler("textDocument/didSave", self._on_did_save) + self:_add_handler("textDocument/didChange", self._on_did_change) + self:_add_handler("textDocument/completion", self._on_completion) + self: + -- self:_add_handler("textDocument/signatureHelp", self._on_signature_help) + -- self:_add_handler("textDocument/definition", self._on_definition) + -- self:_add_handler("textDocument/hover", self._on_hover) +end + ]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(8, 8) + assert.same(node_info.type, ":") + assert.same(node_info.source, ":") + assert.same(node_info.parent_type, "method_index") + assert.same(node_info.preceded_by, "self") + assert.same(node_info.self_type, "MiscHandlers") + + end) + + + it("should work with more real use cases", function() + local content = [[ +function MiscHandlers:_on_hover(params:lsp.Method.Params, id:integer):nil + local pos = params.position as lsp.Position + local node_info, doc = self:_get_node_info(params, pos) + if node_info == nil then + self._lsp_reader_writer:send_rpc(id, { + contents = { "Unknown Token:", " Unable to determine what token is under cursor " }, + range = { + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character), + }, + }) + return + end +end]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 35) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "params") + assert.same(node_info.parent_type, "arg") + assert.same(node_info.parent_source, "params:lsp.Method.Params") + + local node_info = doc:tree_sitter_token(1, 35) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "position") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "params.position") + + local node_info = doc:tree_sitter_token(2, 35) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "_get_node_info") + assert.same(node_info.parent_type, "method_index") + assert.same(node_info.parent_source, "self:_get_node_info") + assert.same(node_info.self_type, "MiscHandlers") + + local node_info = doc:tree_sitter_token(8, 52) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "character") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "pos.character") + + end) + + it("should handle getting function signatures with valid syntax", function() + local content = [[tracing.warning()]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 16) + assert.same(node_info.type, "(") + assert.same(node_info.source, "(") + assert.same(node_info.parent_type, "arguments") + assert.same(node_info.parent_source, "()") + assert.same(node_info.preceded_by, "tracing.warning") + end) + + it("should handle getting function signatures with invalid syntax", function() + local content = [[tracing.warning(]] + + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 16) + assert.same(node_info.type, "(") + assert.same(node_info.source, "(") + assert.same(node_info.parent_type, "ERROR") + assert.same(node_info.parent_source, "tracing.warning(") + assert.same(node_info.preceded_by, "tracing.warning") + end) + + it("", function() + local content = [[ +if indexable_parent_types[node_info.parent_type] then + tks = split_by_symbols(node_info.parent_source, node_info.self_type) +else + tks = split_by_symbols(node_info.source, node_info.self_type) +end]] + local doc = Document("test-uri", content, 1, {}, ServerState()) + + local node_info = doc:tree_sitter_token(0, 16) + assert.same(node_info.type, "identifier") + assert.same(node_info.source, "indexable_parent_types") + assert.same(node_info.parent_type, "index") + assert.same(node_info.parent_source, "indexable_parent_types[node_info.parent_type]") + end) +end) \ No newline at end of file diff --git a/src/teal_language_server/document.tl b/src/teal_language_server/document.tl index bacecaa..200cc29 100644 --- a/src/teal_language_server/document.tl +++ b/src/teal_language_server/document.tl @@ -9,7 +9,7 @@ local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") local ltreesitter = require("ltreesitter") -local teal_parser = ltreesitter.load("./teal.so", "teal") +local teal_parser = ltreesitter.require("parser/teal", "teal") local tl = require("tl") @@ -397,7 +397,7 @@ function Document:type_information_for_tokens(tokens: {string}, y: integer, x: i return nil end -function Document:_parser_token(y: integer, x: integer): Document.NodeInfo +function Document:_tree_sitter_token(y: integer, x: integer): Document.NodeInfo local moved = self._tree_cursor:goto_first_child() local node = self._tree_cursor:current_node() @@ -487,11 +487,11 @@ function Document:_parser_token(y: integer, x: integer): Document.NodeInfo if y == start_point.row and y == end_point.row then if x >= start_point.column and x <= end_point.column then - return self:_parser_token(y, x) + return self:_tree_sitter_token(y, x) end elseif y >= start_point.row and y <= end_point.row then - return self:_parser_token(y, x) + return self:_tree_sitter_token(y, x) end moved = self._tree_cursor:goto_next_sibling() @@ -499,9 +499,9 @@ function Document:_parser_token(y: integer, x: integer): Document.NodeInfo end end -function Document:parser_token(y: integer, x: integer): Document.NodeInfo +function Document:tree_sitter_token(y: integer, x: integer): Document.NodeInfo self._tree_cursor:reset(self._tree:root()) - return self:_parser_token(y, x) + return self:_tree_sitter_token(y, x) end class.setup(Document, "Document", { diff --git a/src/teal_language_server/misc_handlers.tl b/src/teal_language_server/misc_handlers.tl index a009542..a1f506b 100644 --- a/src/teal_language_server/misc_handlers.tl +++ b/src/teal_language_server/misc_handlers.tl @@ -167,7 +167,7 @@ function MiscHandlers:_get_node_info(params:lsp.Method.Params, pos: lsp.Position end tracing.warning(_module_name, "Received request for completion at position: {}", {pos}) - local node_info = doc:parser_token(pos.line, pos.character) + local node_info = doc:tree_sitter_token(pos.line, pos.character) if node_info == nil then tracing.warning(_module_name, "Unable to retrieve node info from tree-sitter parser", {}) return nil diff --git a/teal-language-server-0.0.5-1.rockspec b/teal-language-server-0.0.5-1.rockspec index a65565a..4921fb0 100644 --- a/teal-language-server-0.0.5-1.rockspec +++ b/teal-language-server-0.0.5-1.rockspec @@ -1,16 +1,20 @@ rockspec_format = "3.0" + package = "teal-language-server" version = "0.0.5-1" + source = { url = "git+https://github.com/teal-language/teal-language-server.git", branch = "main" } + description = { summary = "A language server for the Teal language", detailed = "A language server for the Teal language", homepage = "https://github.com/teal-language/teal-language-server", license = "MIT" } + dependencies = { "luafilesystem", "tl", @@ -19,7 +23,18 @@ dependencies = { "inspect", "luv", "lusc_luv >= 4.0", + "ltreesitter", + "tree-sitter-cli", + "tree-sitter-teal", } + +test_dependencies = { "busted~>2" } + +test = { + type = "busted", + flags = {"-m", "gen/?.lua"}, +} + build = { type = "builtin", modules = {