diff --git a/.gitignore b/.gitignore index f0220af..66673bf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ # install dirs on CI machine /.install /.lua + +**.so diff --git a/gen/teal_language_server.sh b/gen/teal_language_server.sh new file mode 100755 index 0000000..91431cc --- /dev/null +++ b/gen/teal_language_server.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$0")" +lua teal_language_server/main.lua "$@" diff --git a/gen/teal_language_server/document.lua b/gen/teal_language_server/document.lua index 8516fd0..e5d4c51 100644 --- a/gen/teal_language_server/document.lua +++ b/gen/teal_language_server/document.lua @@ -1,4 +1,4 @@ -local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local assert = _tl_compat and _tl_compat.assert or assert; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local _module_name = "document" +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local _module_name = "document" local ServerState = require("teal_language_server.server_state") local Uri = require("teal_language_server.uri") @@ -7,7 +7,9 @@ local LspReaderWriter = require("teal_language_server.lsp_reader_writer") local class = require("teal_language_server.class") local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") -local util = require("teal_language_server.util") + +local ltreesitter = require("ltreesitter") +local teal_parser = ltreesitter.load("./teal.so", "teal") local tl = require("tl") @@ -30,7 +32,38 @@ local tl = require("tl") -local Document = {} + + + + + +local Document = {NodeInfo = {}, } + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -54,6 +87,8 @@ function Document:__init(uri, content, version, lsp_reader_writer, server_state) self._version = version self._lsp_reader_writer = lsp_reader_writer self._server_state = server_state + self._tree = teal_parser:parse_string(self._content) + self._tree_cursor = self._tree:root():create_cursor() end @@ -76,34 +111,11 @@ local function filter(t, pred) return pass, fail end -local function binary_search(list, item, cmp) - local len = #list - local mid - local s, e = 1, len - while s <= e do - mid = math.floor((s + e) / 2) - local val = list[mid] - local res = cmp(val, item) - if res then - if mid == len then - return mid, val - else - if not cmp(list[mid + 1], item) then - return mid, val - end - end - s = mid + 1 - else - e = mid - 1 - end - end -end - local function is_lua(fname) return fname:sub(-4) == ".lua" end -function Document:get_tokens() +function Document:_get_tokens() local cache = self._cache if not cache.tokens then cache.tokens, cache.err_tokens = tl.lex(self._content, self._uri.path) @@ -115,26 +127,20 @@ function Document:get_tokens() end local parse_prog = tl.parse_program -function Document:get_ast() - local tks, err_tks = self:get_tokens() - if #err_tks > 0 then - return - end - +function Document:_get_ast(tokens) local cache = self._cache if not cache.ast then local _ cache.parse_errors = {} - cache.ast, _ = parse_prog(tks, cache.parse_errors) + cache.ast, _ = parse_prog(tokens, cache.parse_errors) + tracing.debug(_module_name, "parse_prog errors: " .. #cache.parse_errors) end return cache.ast, cache.parse_errors end local type_check = tl.type_check -function Document:get_result() - local ast, errs = self:get_ast() - local found_errors = #errs > 0 +function Document:_get_result(ast) local cache = self._cache if not cache.result then tracing.info(_module_name, "Type checking document {}", { self._uri.path }) @@ -144,20 +150,40 @@ function Document:get_result() env = self._server_state:get_env(), }) end - return cache.result, found_errors + return cache.result end function Document:get_type_report() - local _result, has_errors = self:get_result() local env = self._server_state:get_env() + return env.reporter:get_report() +end - return env.reporter:get_report(), env.reporter, has_errors +local function _get_node_at(ast, y, x) + for _, node in ipairs(ast) do + if node.y == y and node.x == x then + return node + end + end end -local function _strip_trailing_colons(text) +function Document:get_ast_node_at(type_info) + if type_info.file == "" then + return _get_node_at(self:_get_ast(), type_info.y, type_info.x) + end - text = text:gsub(":\n", ":a\n"):gsub(":\r\n", ":a\r\n") - return text + local loaded_file = self._server_state:get_env().loaded[type_info.file] + if loaded_file == nil then return nil end + return _get_node_at(loaded_file.ast, type_info.y, type_info.x) +end + +function Document:get_function_args_string(type_info) + local node = self:get_ast_node_at(type_info) + if node == nil then return nil end + local output = {} + for _, arg_info in ipairs(node.args) do + table.insert(output, arg_info.tk) + end + return output end function Document:clear_cache() @@ -171,40 +197,19 @@ function Document:update_text(text, version) if not version or not self._version or self._version < version then self:clear_cache() - - - - - self._content = _strip_trailing_colons(text) - + self._content = text self._content_lines = nil if version then self._version = version end end -end - -local get_raw_token_at = tl.get_token_at - -local function get_token_at(tks, y, x) - local _, found = binary_search( - tks, nil, - function(tk) - return tk.y < y or - (tk.y == y and tk.x <= x) - end) - - - if found and - found.y == y and - found.x <= x and x < found.x + #found.tk then - - return found - end + self._tree = teal_parser:parse_string(self._content) + self._tree_cursor = self._tree:root():create_cursor() end +local get_raw_token_at = tl.get_token_at local function make_diagnostic_from_error(tks, err, severity) local x, y = err.x, err.y local err_tk = get_raw_token_at(tks, y, x) @@ -250,7 +255,7 @@ local function imap(t, fn, start, finish) end function Document:process_and_publish_results() - local tks, err_tks = self:get_tokens() + local tks, err_tks = self:_get_tokens() if #err_tks > 0 then self:_publish_diagnostics(imap(err_tks, function(t) return { @@ -265,7 +270,7 @@ function Document:process_and_publish_results() return end - local _, parse_errs = self:get_ast() + local ast, parse_errs = self:_get_ast(tks) if #parse_errs > 0 then self:_publish_diagnostics(imap(parse_errs, function(e) return make_diagnostic_from_error(tks, e, "Error") @@ -275,8 +280,8 @@ function Document:process_and_publish_results() local diags = {} local fname = self._uri.path - local result, has_errors = self:get_result() - assert(not has_errors) + local result = self:_get_result(ast) + local config = self._server_state.config local disabled_warnings = set(config.disable_warnings or {}) local warning_errors = set(config.warning_error or {}) @@ -298,278 +303,205 @@ function Document:process_and_publish_results() self:_publish_diagnostics(diags) end -function Document:get_type_info_for_symbol(identifier, where) - local tr, _ = self:get_type_report() - local symbols = tl.symbols_in_scope(tr, where.line + 1, where.character + 1, self._uri.path) - local type_id = symbols[identifier] - local result = nil - - if type_id ~= nil then - result = tr.types[type_id] - end - - if result == nil then - result = tr.types[tr.globals[identifier]] - end - - if result == nil then - tracing.warning(_module_name, "Failed to find type id for identifier '{}'. Available symbols: Locals: {}. Globals: {}", { identifier, symbols, tr.globals }) +function Document:resolve_type_ref(type_number) + local tr = self:get_type_report() + local type_info = tr.types[type_number] + if type_info.ref then + return self:resolve_type_ref(type_info.ref) else - tracing.debug(_module_name, "Successfully found type id for given identifier '{}'", { identifier }) + return type_info end - - return result end -function Document:type_information_for_token(token) - local tr, _ = self:get_type_report() +function Document:_quick_get(tr, last_token) - local symbols = tl.symbols_in_scope(tr, token.y, token.x, self._uri.path) - local type_id = symbols[token.tk] - local local_type_info = tr.types[type_id] - - if local_type_info then - tracing.trace(_module_name, "Successfully found type info by raw token in local scope", {}) - return local_type_info + local file = tr.by_pos[self._uri.path] + if file == nil then + tracing.warning(_module_name, "selfchecker: the file dissappeared?") + return nil end - local global_type_info = tr.types[tr.globals[token.tk]] - - if global_type_info then - tracing.trace(_module_name, "Successfully found type info by raw token in globals table", {}) - return global_type_info + local line = file[last_token.y] or file[last_token.y - 1] or file[last_token.y + 1] + if line == nil then + tracing.warning(_module_name, "selfchecker: the file dissappeared?") + return nil end - tracing.warning(_module_name, "Failed to find type info at given position", {}) - return nil -end - -function Document:_get_content_lines() - if self._content_lines == nil then - self._content_lines = util.string_split(self._content, "\n") + local type_ref = line[last_token.x] or line[last_token.x - 1] or line[last_token.x + 1] + if type_ref == nil then + tracing.warning(_module_name, "selfchecker: couldn't find the typeref") + return nil end - return self._content_lines + return self:resolve_type_ref(type_ref) end -function Document:get_line(line) - return self:_get_content_lines()[line + 1] -end +function Document:type_information_for_tokens(tokens, y, x) + local tr = self:get_type_report() -local function extract_word(str, index) - local start_index = index - local end_index = index + local type_info - while start_index > 1 and string.match(string.sub(str, start_index - 1, start_index - 1), "[%w_]") do - start_index = start_index - 1 + + local scope_symbols = tl.symbols_in_scope(tr, y + 1, x + 1, self._uri.path) + if #tokens == 0 then + local out = {} + for key, value in pairs(scope_symbols) do out[key] = value end + for key, value in pairs(tr.globals) do out[key] = value end + type_info = { + fields = out, + } + return type_info + end + local type_id = scope_symbols[tokens[1]] + tracing.warning(_module_name, "tokens[1]: " .. tokens[1]) + if type_id ~= nil then + type_info = self:resolve_type_ref(type_id) end - while end_index <= #str and string.match(string.sub(str, end_index, end_index), "[%w_]") do - end_index = end_index + 1 + if type_info == nil then + type_info = tr.types[tr.globals[tokens[1]]] end - return string.sub(str, start_index, end_index - 1) -end + if type_info == nil then + tracing.warning(_module_name, "Unable to find type info in global table as well..") + end -function Document:_try_lookup_from_deref(line_no, char_pos, line_info, tr) + tracing.warning(_module_name, "What is this type_info:" .. tostring(type_info)) - local test_char = char_pos - 1 - local closest_type_id + if type_info and #tokens > 1 then + for i = 2, #tokens do + tracing.trace(_module_name, "tokens[i]: " .. tokens[i]) - while test_char > 1 do - closest_type_id = line_info[test_char] - if closest_type_id ~= nil then - break - end - test_char = test_char - 1 - end + if type_info.fields then + type_info = self:resolve_type_ref(type_info.fields[tokens[i]]) - if closest_type_id == nil then - tracing.debug(_module_name, "Failed to find closest type id", {}) - return nil - end + elseif type_info.values and i == #tokens then + type_info = self:resolve_type_ref(type_info.values) - local parent_type_info = tr.types[closest_type_id] - if parent_type_info == nil then - return nil - end - local line_str = self:get_line(line_no - 1) - local word_under_cursor = extract_word(line_str, char_pos) - if parent_type_info.ref then - local real_type_info = tr.types[parent_type_info.ref] + end - if real_type_info.fields then - return real_type_info.fields[word_under_cursor] + if type_info == nil then break end end - - return nil end - if parent_type_info.fields then - return parent_type_info.fields[word_under_cursor] + if type_info then + tracing.trace(_module_name, "Successfully found type info", {}) + return type_info end + tracing.warning(_module_name, "Failed to find type info at given position", {}) return nil end -function Document:type_information_at(where) - local tr, _ = self:get_type_report() - local file_info = tr.by_pos[self._uri.path] +function Document:_parser_token(y, x) + local moved = self._tree_cursor:goto_first_child() + local node = self._tree_cursor:current_node() - if file_info == nil then - tracing.warning(_module_name, "Could not find file info for path '{}'", { self._uri.path }) - return nil - end + if moved == false then + self._tree_cursor:goto_parent() + local parent_node = self._tree_cursor:current_node() - local line_info = file_info[where.line] + local out = { + type = node:type(), + source = node:source(), + parent_type = parent_node:type(), + parent_source = parent_node:source(), + } - if line_info == nil then - tracing.warning(_module_name, "Could not find line info for file '{}' at line '{}'", { self._uri.path, where.line }) - return nil - end - tracing.trace(_module_name, "Found line info: {}. Checking character {}", { line_info, where.character }) + if node:type() == "." or node:type() == ":" then + local prev = node:prev_sibling() + if prev then + out.preceded_by = prev:source() + else + parent_node = parent_node:prev_sibling() + if parent_node:child_count() > 0 then + out.preceded_by = parent_node:child(parent_node:child_count() - 1):source() + else + out.preceded_by = parent_node:source() + end + end - local type_id = line_info[where.character] or line_info[where.character - 1] or line_info[where.character + 1] - if type_id == nil then - type_id = self:_try_lookup_from_deref(where.line, where.character, line_info, tr) + elseif node:type() == "(" then + if parent_node:type() == "arguments" then + self._tree_cursor:goto_parent() + local function_call = self._tree_cursor:current_node():child_by_field_name("called_object") + if function_call then + out.preceded_by = function_call:source() + end - if type_id == nil then - tracing.warning(_module_name, "Could not find type id for file {} at position {}, line info {}", { self._uri.path, where, line_info }) - return nil + elseif parent_node:type() == "ERROR" then + for child in parent_node:children() do + if child:name() == "index" then + out.preceded_by = child:source() + break + end + end + end end - end - - tracing.trace(_module_name, "Successfully found type id {}", { type_id }) - - local type_info = tr.types[type_id] - - if type_info == nil then - tracing.warning(_module_name, "Could not find type info for type id '{}'", { type_id }) - return nil - end - - tracing.trace(_module_name, "Successfully found type info: {}", { type_info }) - if type_info.str == "string" then - - tracing.trace(_module_name, "Hackily changed type info to string as a special case", {}) - return (self._server_state:get_env().globals["string"])["t"] - end + if out.preceded_by == "self" or + out.source:find("self[%.%:]") or + out.parent_source:find("self[%.%:]") then + + while parent_node:type() ~= "program" do + self._tree_cursor:goto_parent() + parent_node = self._tree_cursor:current_node() + if parent_node:type() == "function_statement" then + local function_name = parent_node:child_by_field_name("name") + if function_name then + local base_name = function_name:child_by_field_name("base") + out.self_type = base_name:source() + break + end + elseif parent_node:type() == "ERROR" then + + for child in parent_node:children() do + if child:name() == "function_name" then + out.self_type = child:child_by_field_name("base"):source() + break + end + end + end + end - local canonical_type_info = tr.types[type_info.ref] + end - if canonical_type_info ~= nil then - tracing.trace(_module_name, "Successfully found type info from ref field: {}", { canonical_type_info }) - return canonical_type_info - end + return out - return type_info -end - -local function indent(n) - return (" "):rep(n) -end -local function ti(list, ...) - for i = 1, select("#", ...) do - table.insert(list, (select(i, ...))) end -end -function Document:show_type(info, depth) - if not info then return "???" end - depth = depth or 1 - if depth > 4 then - return "..." - end + local start_point = node:start_point() + local end_point = node:end_point() - local out = {} + while moved do + start_point = node:start_point() + end_point = node:end_point() - local function ins(...) - ti(out, ...) - end - - local tr, _ = self:get_type_report() - - local function show_record_field(name, field_id) - local field = {} - ti(field, indent(depth)) - local field_type = tr.types[field_id] - if field_type.str:match("^type ") then - ti(field, "type ", name, " = ", (self:show_type(field_type, depth + 1):gsub("^type ", ""))) - else - ti(field, name, ": ", self:show_type(field_type, depth + 1)) - end - ti(field, "\n") - return table.concat(field) - end + 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) + end - local function show_record_fields(fields) - if not fields then - ins("--???\n") - return - end - local f = {} - for name, field_id in pairs(fields) do - ti(f, show_record_field(name, field_id)) - end - local function get_name(s) - return (s:match("^%s*type ([^=]+)") or s:match("^%s*([^:]+)")):lower() + elseif y >= start_point.row and y <= end_point.row then + return self:_parser_token(y, x) end - table.sort(f, function(a, b) - return get_name(a) < get_name(b) - end) - for _, field in ipairs(f) do - ins(field) - end - end - if info.ref then - return info.str .. " => " .. self:show_type(tr.types[info.ref], depth + 1) - elseif info.str == "type record" or info.str == "record" then - ins(info.str) - if not info.fields then - ins(" ??? end") - return table.concat(out) - end - ins("\n") - show_record_fields(info.fields) - ins(indent(depth - 1)) - ins("end") - return table.concat(out) - elseif info.str == "type enum" then - ins("enum\n") - if info.enums then - for _, str in ipairs(info.enums) do - ins(indent(depth)) - ins(string.format("%q\n", str)) - end - else - ins(indent(depth)) - ins("--???") - ins("\n") - end - ins(indent(depth - 1)) - ins("end") - return table.concat(out) - else - return info.str + moved = self._tree_cursor:goto_next_sibling() + node = self._tree_cursor:current_node() end end -function Document:raw_token_at(where) - return get_raw_token_at(self:get_tokens(), where.line + 1, where.character + 1) -end - -function Document:token_at(where) - return get_token_at(self:get_tokens(), where.line + 1, where.character + 1) +function Document:parser_token(y, x) + self._tree_cursor:reset(self._tree:root()) + return self:_parser_token(y, x) end class.setup(Document, "Document", { diff --git a/gen/teal_language_server/lsp.lua b/gen/teal_language_server/lsp.lua index c6bf9fc..f991cc9 100644 --- a/gen/teal_language_server/lsp.lua +++ b/gen/teal_language_server/lsp.lua @@ -3,6 +3,8 @@ +local tl = require("tl") + local lsp = {Message = {ResponseError = {}, }, Position = {}, Range = {}, Location = {}, Diagnostic = {}, Method = {}, TextDocument = {}, TextDocumentContentChangeEvent = {}, CompletionContext = {}, } @@ -104,6 +106,37 @@ local lsp = {Message = {ResponseError = {}, }, Position = {}, Range = {}, Locati + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -158,6 +191,66 @@ lsp.completion_trigger_kind = { TriggerForIncompleteCompletions = 3, } +lsp.completion_item_kind = { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} + + + +lsp.typecodes_to_kind = { + + [tl.typecodes.NIL] = lsp.completion_item_kind.Variable, + [tl.typecodes.NUMBER] = lsp.completion_item_kind.Variable, + [tl.typecodes.BOOLEAN] = lsp.completion_item_kind.Variable, + [tl.typecodes.STRING] = lsp.completion_item_kind.Variable, + [tl.typecodes.TABLE] = lsp.completion_item_kind.Struct, + [tl.typecodes.FUNCTION] = lsp.completion_item_kind.Function, + [tl.typecodes.USERDATA] = lsp.completion_item_kind.Variable, + [tl.typecodes.THREAD] = lsp.completion_item_kind.Variable, + + [tl.typecodes.INTEGER] = lsp.completion_item_kind.Variable, + [tl.typecodes.ENUM] = lsp.completion_item_kind.Enum, + [tl.typecodes.ARRAY] = lsp.completion_item_kind.Struct, + [tl.typecodes.RECORD] = lsp.completion_item_kind.Reference, + [tl.typecodes.MAP] = lsp.completion_item_kind.Struct, + [tl.typecodes.TUPLE] = lsp.completion_item_kind.Struct, + [tl.typecodes.INTERFACE] = lsp.completion_item_kind.Interface, + [tl.typecodes.SELF] = lsp.completion_item_kind.Struct, + [tl.typecodes.POLY] = lsp.completion_item_kind.Function, + [tl.typecodes.UNION] = lsp.completion_item_kind.TypeParameter, + + [tl.typecodes.NOMINAL] = lsp.completion_item_kind.Variable, + [tl.typecodes.TYPE_VARIABLE] = lsp.completion_item_kind.Reference, + + [tl.typecodes.ANY] = lsp.completion_item_kind.Variable, + [tl.typecodes.UNKNOWN] = lsp.completion_item_kind.Variable, + [tl.typecodes.INVALID] = lsp.completion_item_kind.Text, +} + function lsp.position(y, x) return { character = x, diff --git a/gen/teal_language_server/lsp_formatter.lua b/gen/teal_language_server/lsp_formatter.lua new file mode 100644 index 0000000..5e4e875 --- /dev/null +++ b/gen/teal_language_server/lsp_formatter.lua @@ -0,0 +1,135 @@ +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local tl = require("tl") + +local Document = require("teal_language_server.document") + +local lsp_formatter = {Documentation = {}, SignatureHelp = {SignatureParameter = {}, Signature = {}, }, } + + + + + + + + + + + + + + + + + + + + + + + + +local function _split_not_in_parenthesis(str, start, finish) + local parens_count = 0 + local i = start + local output = {} + local start_field = i + while i <= finish do + if str:sub(i, i) == "(" then + parens_count = parens_count + 1 + end + if str:sub(i, i) == ")" then + parens_count = parens_count - 1 + end + if str:sub(i, i) == "," and parens_count == 0 then + output[#output + 1] = str:sub(start_field, i) + start_field = i + 2 + end + i = i + 1 + end + table.insert(output, str:sub(start_field, i)) + return output +end + +function lsp_formatter.create_function_string(type_string, arg_names, tk) + local _, _, types, args, returns = type_string:find("^function(.-)(%b())(.-)$") + local output = {} + if tk then output[1] = tk else output[1] = "function" end + output[2] = types + output[3] = "(" + + for i, argument in ipairs(_split_not_in_parenthesis(args, 2, #args - 2)) do + output[#output + 1] = arg_names[i] + output[#output + 1] = ": " + output[#output + 1] = argument + output[#output + 1] = " " + end + output[#output] = ")" + output[#output + 1] = returns + return table.concat(output) +end + + + + + + + + + + + + + +function lsp_formatter.show_type(node_info, type_info, doc) + local output = { kind = "markdown" } + local sb = { strings = {} } + table.insert(sb.strings, "```teal") + + if type_info.t == tl.typecodes.FUNCTION then + local args = doc:get_function_args_string(type_info) + if args ~= nil then + table.insert(sb.strings, "function " .. lsp_formatter.create_function_string(type_info.str, args, node_info.source)) + else + table.insert(sb.strings, node_info.source .. ": " .. type_info.str) + end + + elseif type_info.t == tl.typecodes.POLY then + for i, type_ref in ipairs(type_info.types) do + local func_info = doc:resolve_type_ref(type_ref) + local args = doc:get_function_args_string(func_info) + if args ~= nil then + table.insert(sb.strings, "function " .. lsp_formatter.create_function_string(func_info.str, args, node_info.source)) + else + local replaced_function = func_info.str:gsub("^function", node_info.source) + table.insert(sb.strings, replaced_function) + end + if i < #type_info.types then + table.insert(sb.strings, "```") + table.insert(sb.strings, "or") + table.insert(sb.strings, "```teal") + end + end + + elseif type_info.t == tl.typecodes.ENUM then + table.insert(sb.strings, "enum " .. type_info.str) + for _, _enum in ipairs(type_info.enums) do + table.insert(sb.strings, ' "' .. _enum .. '"') + end + table.insert(sb.strings, "end") + + elseif type_info.t == tl.typecodes.RECORD then + table.insert(sb.strings, "record " .. type_info.str) + for key, type_ref in pairs(type_info.fields) do + local type_ref_info = doc:resolve_type_ref(type_ref) + table.insert(sb.strings, ' ' .. key .. ': ' .. type_ref_info.str) + end + table.insert(sb.strings, "end") + + else + table.insert(sb.strings, node_info.source .. ": " .. type_info.str) + end + table.insert(sb.strings, "```") + output.value = table.concat(sb.strings, "\n") + return output +end + +return lsp_formatter diff --git a/gen/teal_language_server/lsp_reader_writer.lua b/gen/teal_language_server/lsp_reader_writer.lua index 821265f..4cffd2e 100644 --- a/gen/teal_language_server/lsp_reader_writer.lua +++ b/gen/teal_language_server/lsp_reader_writer.lua @@ -2,7 +2,7 @@ local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 th local StdinReader = require("teal_language_server.stdin_reader") local lsp = require("teal_language_server.lsp") -local json = require("dkjson") +local json = require("cjson") local uv = require("luv") local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") diff --git a/gen/teal_language_server/misc_handlers.lua b/gen/teal_language_server/misc_handlers.lua index 91ed28b..5c6ffc4 100644 --- a/gen/teal_language_server/misc_handlers.lua +++ b/gen/teal_language_server/misc_handlers.lua @@ -1,10 +1,11 @@ -local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local _module_name = "misc_handlers" +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local _module_name = "misc_handlers" local EnvUpdater = require("teal_language_server.env_updater") local args_parser = require("teal_language_server.args_parser") local TraceStream = require("teal_language_server.trace_stream") local DocumentManager = require("teal_language_server.document_manager") +local Document = require("teal_language_server.document") local ServerState = require("teal_language_server.server_state") local LspReaderWriter = require("teal_language_server.lsp_reader_writer") local Path = require("teal_language_server.path") @@ -15,6 +16,14 @@ local uv = require("luv") local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") local class = require("teal_language_server.class") +local tl = require("tl") +local lsp_formatter = require("teal_language_server.lsp_formatter") + +local indexable_parent_types = { + ["index"] = true, + ["method_index"] = true, + ["function_name"] = true, +} local MiscHandlers = {} @@ -40,12 +49,12 @@ function MiscHandlers:__init(lsp_events_manager, lsp_reader_writer, server_state self._trace_stream = trace_stream self._cl_args = args self._env_updater = env_updater + end function MiscHandlers:_on_initialize(params, id) asserts.that(not self._has_handled_initialize) self._has_handled_initialize = true - local root_dir_str if params.rootUri then @@ -100,10 +109,12 @@ end function MiscHandlers:_on_did_save(params) local td = params.textDocument local doc = self._document_manager:get(Uri.parse(td.uri)) + if not doc then tracing.warning(_module_name, "Unable to find document: {}", { td.uri }) return end + doc:update_text(params.text, td.version) @@ -124,15 +135,27 @@ function MiscHandlers:_on_did_change(params) doc:process_and_publish_results() end -function MiscHandlers:_on_completion(params, id) - local context = params.context - +local function split_by_symbols(input, self_type, stop_at) + local t = {} + for str in string.gmatch(input, "([^%.%:]+)") do + if str == "self" then + table.insert(t, self_type) + else + table.insert(t, str) + end + if stop_at and stop_at == str then + break + end + end + return t +end +function MiscHandlers:_get_node_info(params, pos) + local context = params.context - if context.triggerKind ~= 2 then + if context and context.triggerKind ~= lsp.completion_trigger_kind.TriggerCharacter then tracing.warning(_module_name, "Ignoring completion request given kind: {}", { context.triggerKind }) - self._lsp_reader_writer:send_rpc(id, nil) - return + return nil end local td = params.textDocument @@ -140,60 +163,123 @@ function MiscHandlers:_on_completion(params, id) if not doc then tracing.warning(_module_name, "No doc found for completion request", {}) + return nil + end + + tracing.warning(_module_name, "Received request for completion at position: {}", { pos }) + local node_info = doc:parser_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 + end + tracing.warning(_module_name, "Found node info at pos", node_info) + return node_info, doc +end + +function MiscHandlers:_on_completion(params, id) + local pos = params.position + local node_info, doc = self:_get_node_info(params, pos) + if node_info == nil then self._lsp_reader_writer:send_rpc(id, nil) return end - local pos = params.position - pos.character = pos.character - 2 + tracing.warning(_module_name, "Got nodeinfo: {}", node_info) + + local tks + + + + if node_info.type == "." or node_info.type == ":" then + tks = split_by_symbols(node_info.preceded_by, node_info.self_type) + tracing.warning(_module_name, "Received request for completion at character: {}", { tks }) + + + elseif node_info.type == "identifier" then + + 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 - tracing.info(_module_name, "Received request for completion at position: {}", { pos }) - local tk = doc:token_at(pos) - if not tk then - tracing.warning(_module_name, "Could not find token at given position", {}) + + tks[#tks] = nil + + + + if node_info.parent_type == "var" or + node_info.parent_type == "simple_type" or + node_info.parent_type == "table_type" then + self._lsp_reader_writer:send_rpc(id, nil) + return + end + + else self._lsp_reader_writer:send_rpc(id, nil) return end - local token_pos = lsp.position(tk.y, tk.x) - tracing.trace(_module_name, "Found actual token {} at position: {}", { tk.tk, token_pos }) - - local type_info = doc:type_information_at(token_pos) local items = {} + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) if not type_info then - tracing.trace(_module_name, "No type information found at calculated token position {}. Attempting to get type information by raw token instead.", { token_pos }) - - type_info = doc:type_information_for_token(tk) - - if not type_info then - tracing.warning(_module_name, "Also failed to find type type_info based on token", {}) - end + tracing.warning(_module_name, "Also failed to find type type_info based on token", {}) end if type_info then - tracing.trace(_module_name, "Successfully found type type_info '{}'", { type_info }) + tracing.warning(_module_name, "Successfully found type type_info '{}'", type_info) + local tr = doc:get_type_report() if type_info.ref then - local tr, _ = doc:get_type_report() - local real_type_info = tr.types[type_info.ref] - if real_type_info.fields then - for key, _ in pairs(real_type_info.fields) do - table.insert(items, { label = key }) + type_info = doc:resolve_type_ref(type_info.ref) + end + + + if type_info.t == tl.typecodes.STRING then + type_info = tr.types[tr.globals["string"]] + end + + + + + local original_str = type_info.str + + if type_info.fields then + for key, v in pairs(type_info.fields) do + type_info = doc:resolve_type_ref(v) + + if node_info.type == ":" then + if type_info.t == tl.typecodes.FUNCTION then + + if type_info.args and #type_info.args >= 1 then + local first_arg_type = doc:resolve_type_ref(type_info.args[1][1]) + if first_arg_type.t == tl.typecodes.SELF or (first_arg_type.t == tl.typecodes.NOMINAL and first_arg_type.str == original_str) then + tracing.warning(_module_name, "adding " .. key, {}) + table.insert(items, { label = key, kind = lsp.typecodes_to_kind[type_info.t] }) + else + tracing.warning(_module_name, "type info str: " .. original_str .. "first arg str: " .. first_arg_type.str, {}) + end + end + end + else + table.insert(items, { label = key, kind = lsp.typecodes_to_kind[type_info.t] }) end - else - tracing.warning(_module_name, "Unable to get fields for ref type", {}) end - else - if type_info.fields then - for key, _ in pairs(type_info.fields) do - table.insert(items, { label = key }) + + + elseif type_info.keys then + type_info = doc:resolve_type_ref(type_info.keys) + + if type_info.enums then + for _, enum_value in ipairs(type_info.enums) do + table.insert(items, { label = enum_value, kind = lsp.typecodes_to_kind[type_info.t] }) end - else - tracing.warning(_module_name, "Unable to get fields for type", {}) end + else + tracing.warning(_module_name, "Unable to get fields for ref type", {}) end end @@ -201,50 +287,114 @@ function MiscHandlers:_on_completion(params, id) table.insert(items, { label = "(none)" }) end + tracing.warning(_module_name, "Sending " .. #items .. " back to client", {}) + self._lsp_reader_writer:send_rpc(id, { isIncomplete = false, items = items, }) end -function MiscHandlers:_on_definition(params, id) - local td = params.textDocument - local doc = self._document_manager:get(Uri.parse(td.uri)) +function MiscHandlers:_on_signature_help(params, id) + local pos = params.position + local node_info, doc = self:_get_node_info(params, pos) + if node_info == nil then + self._lsp_reader_writer:send_rpc(id, nil) + return + end - if not doc then - tracing.trace(_module_name, "[on_definition] No document found for given uri", {}) + local output = {} + tracing.warning(_module_name, "Got nodeinfo: {}", node_info) + + local tks + + if node_info.type == "(" then + tks = split_by_symbols(node_info.preceded_by, node_info.self_type) + tracing.warning(_module_name, "Received request for signature help at character: {}", { tks }) + else + self._lsp_reader_writer:send_rpc(id, nil) + return + end + + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) + + if type_info == nil then + self._lsp_reader_writer:send_rpc(id, nil) return end + output.signatures = {} + if type_info.t == tl.typecodes.POLY then + for _, type_ref in ipairs(type_info.types) do + type_info = doc:resolve_type_ref(type_ref) + local args = doc:get_function_args_string(type_info) + if args ~= nil then + local func_str = lsp_formatter.create_function_string(type_info.str, args, node_info.preceded_by) + table.insert(output.signatures, { label = func_str }) + + else + table.insert(output.signatures, { label = type_info.str }) + end + end + else + local args = doc:get_function_args_string(type_info) + if args ~= nil then + local func_str = lsp_formatter.create_function_string(type_info.str, args, node_info.preceded_by) + table.insert(output.signatures, { label = func_str }) + else + table.insert(output.signatures, { label = type_info.str }) + end + end + + tracing.warning(_module_name, "[_on_signature_help] Found type info: {}", { type_info }) + + self._lsp_reader_writer:send_rpc(id, output) +end + +function MiscHandlers:_on_definition(params, id) local pos = params.position - local tk = doc:token_at(pos) - if not tk then - tracing.trace(_module_name, "[on_definition] No token found at given position", {}) + local node_info, doc = self:_get_node_info(params, pos) + if node_info == nil then + self._lsp_reader_writer:send_rpc(id, nil) + return + end + + tracing.trace(_module_name, "Received request for hover at position: {}", { pos }) + + local tks = {} + if node_info.type == "identifier" then + + if indexable_parent_types[node_info.parent_type] then + tks = split_by_symbols(node_info.parent_source, node_info.self_type, node_info.source) + else + tks = split_by_symbols(node_info.source, node_info.self_type) + end + else + tracing.warning(_module_name, "Can't hover over anything that isn't an identifier atm" .. node_info.type, {}) self._lsp_reader_writer:send_rpc(id, nil) return end - local token_pos = lsp.position(tk.y, tk.x) - local info = doc:type_information_at(token_pos) + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) - if not info or info.file == nil then + if not type_info or type_info.file == nil then self._lsp_reader_writer:send_rpc(id, nil) return end - tracing.trace(_module_name, "[on_definition] Found type info: {}", { info }) + tracing.trace(_module_name, "[on_definition] Found type type_info: {}", { type_info }) local file_uri - if #info.file == 0 then + if #type_info.file == 0 then file_uri = doc.uri else local full_path - if Path(info.file):is_absolute() then - full_path = info.file + if Path(type_info.file):is_absolute() then + full_path = type_info.file else - full_path = self._server_state.teal_project_root_dir.value .. "/" .. info.file + full_path = self._server_state.teal_project_root_dir.value .. "/" .. type_info.file end local file_path = Path(full_path) @@ -254,65 +404,74 @@ function MiscHandlers:_on_definition(params, id) self._lsp_reader_writer:send_rpc(id, { uri = Uri.tostring(file_uri), range = { - start = lsp.position(info.y - 1, info.x - 1), - ["end"] = lsp.position(info.y - 1, info.x - 1), + start = lsp.position(type_info.y - 1, type_info.x - 1), + ["end"] = lsp.position(type_info.y - 1, type_info.x - 1), }, }) end function MiscHandlers:_on_hover(params, id) - local td = params.textDocument - local doc = self._document_manager:get(Uri.parse(td.uri)) - - if not doc then - tracing.warning(_module_name, "Failed to find document for given params", {}) - self._lsp_reader_writer:send_rpc(id, nil) + local pos = params.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 - local pos = params.position tracing.trace(_module_name, "Received request for hover at position: {}", { pos }) - local tk = doc:token_at(pos) - if not tk then - tracing.warning(_module_name, "Could not find token at given position", {}) + local tks = {} + if node_info.type == "identifier" then + + if indexable_parent_types[node_info.parent_type] then + tks = split_by_symbols(node_info.parent_source, node_info.self_type, node_info.source) + else + tks = split_by_symbols(node_info.source, node_info.self_type) + end + else + tracing.warning(_module_name, "Can't hover over anything that isn't an identifier atm" .. node_info.type, {}) self._lsp_reader_writer:send_rpc(id, { - contents = { " No info found " }, + contents = { node_info.parent_type, ":", node_info.type }, + range = { + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character + #node_info.source), + }, }) return end - local token_pos = lsp.position(tk.y, tk.x) - tracing.trace(_module_name, "Found actual token '{}' at position: '{}'", { tk.tk, token_pos }) - local type_info = doc:type_information_at(token_pos) + + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) + if not type_info then - tracing.warning(_module_name, "No type information found at calculated token position. Attempting to get type information by raw token instead...", {}) - - type_info = doc:type_information_for_token(tk) - - if not type_info then - tracing.warning(_module_name, "Also failed to find type info based on token", {}) - self._lsp_reader_writer:send_rpc(id, { - contents = { tk.tk .. ":", " No type_info found " }, - range = { - start = lsp.position(token_pos.line, token_pos.character), - ["end"] = lsp.position(token_pos.line, token_pos.character + #tk.tk), - }, - }) - return - end + tracing.warning(_module_name, "Also failed to find type info based on token", {}) + self._lsp_reader_writer:send_rpc(id, { + contents = { node_info.source .. ":", " No type_info found " }, + range = { + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character + #node_info.source), + }, + }) + return end - tracing.trace(_module_name, "Successfully found type_info: {}", { type_info }) + tracing.warning(_module_name, "Successfully found type_info: {}", { type_info }) - local type_str = doc:show_type(type_info) + local type_str = lsp_formatter.show_type(node_info, type_info, doc) self._lsp_reader_writer:send_rpc(id, { - contents = { tk.tk .. ":", type_str }, + contents = type_str, range = { - start = lsp.position(token_pos.line, token_pos.character), - ["end"] = lsp.position(token_pos.line, token_pos.character + #tk.tk), + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character + #node_info.source), }, }) end + function MiscHandlers:_add_handler(name, handler) self._lsp_events_manager:set_handler(name, function(params, id) handler(self, params, id) end) end @@ -325,8 +484,11 @@ function MiscHandlers:initialize() 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:_add_handler("textDocument/definition", self._on_definition) + self:_add_handler("textDocument/signatureHelp", self._on_signature_help) self:_add_handler("textDocument/hover", self._on_hover) + + + self:_add_handler("textDocument/definition", self._on_definition) end class.setup(MiscHandlers, "MiscHandlers", {}) diff --git a/gen/teal_language_server/server_state.lua b/gen/teal_language_server/server_state.lua index aceebd3..863aa49 100644 --- a/gen/teal_language_server/server_state.lua +++ b/gen/teal_language_server/server_state.lua @@ -40,10 +40,12 @@ local capabilities = { }, hoverProvider = true, definitionProvider = true, - completionProvider = { triggerCharacters = { ".", ":" }, }, + signatureHelpProvider = { + triggerCharacters = { "(" }, + }, } function ServerState:_validate_config(c) @@ -200,7 +202,9 @@ end function ServerState:_load_config(root_dir) local config_path = root_dir:join("tlconfig.lua") - asserts.that(config_path:exists()) + if config_path:exists() == false then + return {} + end local success, result = pcall(dofile, config_path.value) diff --git a/gen/teal_language_server/trace_stream.lua b/gen/teal_language_server/trace_stream.lua index 6c8497c..5b32785 100644 --- a/gen/teal_language_server/trace_stream.lua +++ b/gen/teal_language_server/trace_stream.lua @@ -3,7 +3,7 @@ local util = require("teal_language_server.util") local asserts = require("teal_language_server.asserts") local Path = require("teal_language_server.path") local TraceEntry = require("teal_language_server.trace_entry") -local json = require("dkjson") +local json = require("cjson") local uv = require("luv") local class = require("teal_language_server.class") local inspect = require("inspect") diff --git a/src/teal_language_server/document.tl b/src/teal_language_server/document.tl index 39c3866..bacecaa 100644 --- a/src/teal_language_server/document.tl +++ b/src/teal_language_server/document.tl @@ -7,7 +7,9 @@ local LspReaderWriter = require("teal_language_server.lsp_reader_writer" local class = require("teal_language_server.class") local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") -local util = require("teal_language_server.util") + +local ltreesitter = require("ltreesitter") +local teal_parser = ltreesitter.load("./teal.so", "teal") local tl = require("tl") @@ -16,21 +18,36 @@ local record Token x: integer y: integer tk: string + kind: string end -local type Node = any +local interface Node + kind: string + x: integer + y: integer +end local record Cache tokens: {Token} err_tokens: {tl.Error} - ast: Node + ast: {Node} parse_errors: {tl.Error} result: tl.Result end local record Document + record NodeInfo + type: string + source: string + parent_type: string + parent_source: string + preceded_by: string + self_type: string + metamethod __tostring: function(NodeInfo): string + end + uri: Uri _uri: Uri @@ -40,10 +57,26 @@ local record Document _lsp_reader_writer: LspReaderWriter _server_state: ServerState _cache: Cache + _tree: ltreesitter.Tree + _tree_cursor: ltreesitter.Cursor metamethod __call: function(self: Document, uri: Uri, content: string, version: integer, lsp_reader_writer: LspReaderWriter, server_state: ServerState): Document end +local record ArgListNode is Node + record ArgType + typename: string + end + + record ArgInfo + tk: string + opt: boolean + argtype: ArgType + end + + args: {ArgInfo} +end + function Document:__init(uri: Uri, content: string, version: integer, lsp_reader_writer: LspReaderWriter, server_state: ServerState) asserts.is_not_nil(lsp_reader_writer) asserts.is_not_nil(server_state) @@ -54,6 +87,8 @@ function Document:__init(uri: Uri, content: string, version: integer, lsp_reader self._version = version self._lsp_reader_writer = lsp_reader_writer self._server_state = server_state + self._tree = teal_parser:parse_string(self._content) + self._tree_cursor = self._tree:root():create_cursor() end ---@desc @@ -76,34 +111,11 @@ local function filter(t: {Value}, pred: function(Value): boolean): {Value return pass, fail end -local function binary_search(list: {T}, item: U, cmp: function(T, U): boolean): integer, T - local len = #list - local mid: integer - local s, e = 1, len - while s <= e do - mid = math.floor((s + e) / 2) - local val = list[mid] - local res = cmp(val, item) - if res then - if mid == len then - return mid, val - else - if not cmp(list[mid + 1], item) then - return mid, val - end - end - s = mid + 1 - else - e = mid - 1 - end - end -end - local function is_lua(fname: string): boolean return fname:sub(-4) == ".lua" end -function Document:get_tokens(): {Token}, {tl.Error} +function Document:_get_tokens(): {Token}, {tl.Error} local cache = self._cache if not cache.tokens then cache.tokens, cache.err_tokens = tl.lex(self._content, self._uri.path) as ({Token}, {tl.Error}) @@ -114,27 +126,21 @@ function Document:get_tokens(): {Token}, {tl.Error} return cache.tokens, cache.err_tokens end -local parse_prog = tl.parse_program as function({Token}, {tl.Error}, ?string): Node, {string} -function Document:get_ast(): Node, {tl.Error} - local tks, err_tks = self:get_tokens() - if #err_tks > 0 then - return - end - +local parse_prog = tl.parse_program as function({Token}, {tl.Error}, ?string): {Node}, {string} +function Document:_get_ast(tokens?: {Token}): {Node}, {tl.Error} local cache = self._cache if not cache.ast then local _: any cache.parse_errors = {} - cache.ast, _ = parse_prog(tks, cache.parse_errors) + cache.ast, _ = parse_prog(tokens, cache.parse_errors) + tracing.debug(_module_name, "parse_prog errors: " .. #cache.parse_errors) end return cache.ast, cache.parse_errors end -local type_check = tl.type_check as function(Node, tl.TypeCheckOptions): tl.Result +local type_check = tl.type_check as function({Node}, tl.TypeCheckOptions): tl.Result -function Document:get_result(): tl.Result, boolean - local ast, errs = self:get_ast() - local found_errors = #errs > 0 +function Document:_get_result(ast: {Node}): tl.Result local cache = self._cache if not cache.result then tracing.info(_module_name, "Type checking document {}", {self._uri.path}) @@ -144,20 +150,40 @@ function Document:get_result(): tl.Result, boolean env = self._server_state:get_env(), }) end - return cache.result, found_errors + return cache.result end -function Document:get_type_report(): tl.TypeReport, tl.TypeReporter, boolean - local _result, has_errors = self:get_result() -- TODO - is this necessary? +function Document:get_type_report(): tl.TypeReport local env = self._server_state:get_env() + return env.reporter:get_report() +end + +local function _get_node_at(ast: {Node}, y: integer, x: integer): Node + for _, node in ipairs(ast) do + if node.y == y and node.x == x then + return node + end + end +end + +function Document:get_ast_node_at(type_info: tl.TypeInfo): Node + if type_info.file == "" then + return _get_node_at(self:_get_ast(), type_info.y, type_info.x) + end - return env.reporter:get_report(), env.reporter, has_errors + local loaded_file = self._server_state:get_env().loaded[type_info.file] + if loaded_file == nil then return nil end + return _get_node_at(loaded_file.ast as {Node}, type_info.y, type_info.x) end -local function _strip_trailing_colons(text: string): string - -- TODO - remove this hack - text = text:gsub(":\n", ":a\n"):gsub(":\r\n", ":a\r\n") - return text +function Document:get_function_args_string(type_info: tl.TypeInfo): {string} + local node = self:get_ast_node_at(type_info) as ArgListNode + if node == nil then return nil end + local output: {string} = {} + for _, arg_info in ipairs(node.args) do + table.insert(output, arg_info.tk) + end + return output end function Document:clear_cache() @@ -170,41 +196,20 @@ function Document:update_text(text: string, version: integer) if not version or not self._version or self._version < version then self:clear_cache() - -- TODO - this is a hack - -- necessary since without this, teal fails to parse the line and we can't get any - -- info from it at all - -- This helps completion sometimes but also breaks goto labels - self._content = _strip_trailing_colons(text) - -- self._content = text + self._content = text self._content_lines = nil if version then self._version = version end end -end - -local get_raw_token_at = tl.get_token_at as function({Token}, number, number): string - --- This is just a copy and pasted version of tl.get_token_at --- that returns the full token and not just the token string -local function get_token_at(tks: {Token}, y: integer, x: integer): Token - local _, found = binary_search( - tks, nil, - function(tk: Token): boolean - return tk.y < y - or (tk.y == y and tk.x <= x) - end - ) - if found - and found.y == y - and found.x <= x and x < found.x + #found.tk - then - return found - end + -- update tree and tree cursor as well + self._tree = teal_parser:parse_string(self._content) + self._tree_cursor = self._tree:root():create_cursor() end +local get_raw_token_at = tl.get_token_at as function({Token}, number, number): string local function make_diagnostic_from_error(tks: {Token}, err: tl.Error, severity: lsp.Severity): lsp.Diagnostic local x , y = err.x, err.y local err_tk = get_raw_token_at(tks, y, x) @@ -250,7 +255,7 @@ local function imap(t: {V}, fn: function(V): (T), start?: integer, finish? end function Document:process_and_publish_results() - local tks, err_tks = self:get_tokens() + local tks, err_tks = self:_get_tokens() if #err_tks > 0 then self:_publish_diagnostics(imap(err_tks, function(t: tl.Error): lsp.Diagnostic return { @@ -265,7 +270,7 @@ function Document:process_and_publish_results() return end - local _, parse_errs = self:get_ast() + local ast, parse_errs = self:_get_ast(tks) if #parse_errs > 0 then self:_publish_diagnostics(imap(parse_errs, function(e: tl.Error): lsp.Diagnostic return make_diagnostic_from_error(tks, e, "Error") @@ -275,8 +280,8 @@ function Document:process_and_publish_results() local diags : {lsp.Diagnostic} = {} local fname = self._uri.path - local result, has_errors = self:get_result() - assert(not has_errors) + local result = self:_get_result(ast) + local config = self._server_state.config local disabled_warnings = set(config.disable_warnings or {}) local warning_errors = set(config.warning_error or {}) @@ -298,278 +303,205 @@ function Document:process_and_publish_results() self:_publish_diagnostics(diags) end -function Document:get_type_info_for_symbol(identifier:string, where: lsp.Position): tl.TypeInfo - local tr , _ = self:get_type_report() - local symbols = tl.symbols_in_scope(tr, where.line + 1, where.character + 1, self._uri.path) - local type_id = symbols[identifier] - local result:tl.TypeInfo = nil - - if type_id ~= nil then - result = tr.types[type_id] - end - - if result == nil then - result = tr.types[tr.globals[identifier]] - end - - if result == nil then - tracing.warning(_module_name, "Failed to find type id for identifier '{}'. Available symbols: Locals: {}. Globals: {}", {identifier, symbols, tr.globals}) +function Document:resolve_type_ref(type_number: integer): tl.TypeInfo + local tr = self:get_type_report() + local type_info = tr.types[type_number] + if type_info.ref then + return self:resolve_type_ref(type_info.ref) else - tracing.debug(_module_name, "Successfully found type id for given identifier '{}'", {identifier}) + return type_info end - - return result end -function Document:type_information_for_token(token: Token): tl.TypeInfo - local tr , _ = self:get_type_report() - - local symbols = tl.symbols_in_scope(tr, token.y, token.x, self._uri.path) - local type_id = symbols[token.tk] - local local_type_info = tr.types[type_id] - - if local_type_info then - tracing.trace(_module_name, "Successfully found type info by raw token in local scope", {}) - return local_type_info +function Document:_quick_get(tr: tl.TypeReport, last_token: Token): tl.TypeInfo + -- this is _really_ hacky... Gotta figure out these weird off by one errors... + local file = tr.by_pos[self._uri.path] + if file == nil then + tracing.warning(_module_name, "selfchecker: the file dissappeared?") + return nil end - local global_type_info = tr.types[tr.globals[token.tk]] - - if global_type_info then - tracing.trace(_module_name, "Successfully found type info by raw token in globals table", {}) - return global_type_info + local line = file[last_token.y] or file[last_token.y-1] or file[last_token.y+1] + if line == nil then + tracing.warning(_module_name, "selfchecker: the file dissappeared?") + return nil end - tracing.warning(_module_name, "Failed to find type info at given position", {}) - return nil -end - -function Document:_get_content_lines(): {string} - if self._content_lines == nil then - self._content_lines = util.string_split(self._content, "\n") + local type_ref = line[last_token.x] or line[last_token.x-1] or line[last_token.x+1] + if type_ref == nil then + tracing.warning(_module_name, "selfchecker: couldn't find the typeref") + return nil end - return self._content_lines -end - -function Document:get_line(line: integer): string - return self:_get_content_lines()[line + 1] + return self:resolve_type_ref(type_ref) end -local function extract_word(str:string, index:integer):string - local start_index = index - local end_index = index - - -- Move backwards to find the start of the word - while start_index > 1 and string.match(string.sub(str, start_index - 1, start_index - 1), "[%w_]") do - start_index = start_index - 1 - end - - -- Move forwards to find the end of the word - while end_index <= #str and string.match(string.sub(str, end_index, end_index), "[%w_]") do - end_index = end_index + 1 - end - - return string.sub(str, start_index, end_index - 1) -end +function Document:type_information_for_tokens(tokens: {string}, y: integer, x: integer): tl.TypeInfo + local tr = self:get_type_report() -function Document:_try_lookup_from_deref(line_no:integer, char_pos:integer, line_info:{integer: integer}, tr:tl.TypeReport):integer + -- try the quick get (works well for self and raw types) + local type_info: tl.TypeInfo - local test_char = char_pos-1 - local closest_type_id:integer + -- try and find it in scope + local scope_symbols = tl.symbols_in_scope(tr, y+1, x+1, self._uri.path) + if #tokens == 0 then + local out = {} + for key, value in pairs(scope_symbols) do out[key] = value end + for key, value in pairs(tr.globals) do out[key] = value end + type_info = { + fields = out + } + return type_info + end + local type_id = scope_symbols[tokens[1]] + tracing.warning(_module_name, "tokens[1]: " .. tokens[1]) + if type_id ~= nil then + type_info = self:resolve_type_ref(type_id) + end - while test_char > 1 do - closest_type_id = line_info[test_char] - if closest_type_id ~= nil then - break - end - test_char = test_char - 1 + -- might be global instead + if type_info == nil then + type_info = tr.types[tr.globals[tokens[1]] ] end - if closest_type_id == nil then - tracing.debug(_module_name, "Failed to find closest type id", {}) - return nil + if type_info == nil then + tracing.warning(_module_name, "Unable to find type info in global table as well..") end - local parent_type_info = tr.types[closest_type_id] + tracing.warning(_module_name, "What is this type_info:" .. tostring(type_info)) - if parent_type_info == nil then - return nil - end + if type_info and #tokens > 1 then + for i = 2, #tokens do + tracing.trace(_module_name, "tokens[i]: " .. tokens[i]) - local line_str = self:get_line(line_no-1) - local word_under_cursor = extract_word(line_str, char_pos) + if type_info.fields then + type_info = self:resolve_type_ref(type_info.fields[tokens[i]]) - if parent_type_info.ref then - local real_type_info = tr.types[parent_type_info.ref] + elseif type_info.values and i == #tokens then + type_info = self:resolve_type_ref(type_info.values) - if real_type_info.fields then - return real_type_info.fields[word_under_cursor] - end + -- else + -- tracing.warning(_module_name, "Something odd is going on here bruv '{}'", type_info) - return nil + end + + if type_info == nil then break end + end end - if parent_type_info.fields then - return parent_type_info.fields[word_under_cursor] + if type_info then + tracing.trace(_module_name, "Successfully found type info", {}) + return type_info end + tracing.warning(_module_name, "Failed to find type info at given position", {}) return nil end -function Document:type_information_at(where: lsp.Position): tl.TypeInfo - local tr , _ = self:get_type_report() - local file_info = tr.by_pos[self._uri.path] +function Document:_parser_token(y: integer, x: integer): Document.NodeInfo + local moved = self._tree_cursor:goto_first_child() + local node = self._tree_cursor:current_node() - if file_info == nil then - tracing.warning(_module_name, "Could not find file info for path '{}'", {self._uri.path}) - return nil - end - - local line_info = file_info[where.line] - - if line_info == nil then - tracing.warning(_module_name, "Could not find line info for file '{}' at line '{}'", {self._uri.path, where.line}) - return nil - end + if moved == false then + self._tree_cursor:goto_parent() + local parent_node = self._tree_cursor:current_node() - tracing.trace(_module_name, "Found line info: {}. Checking character {}", {line_info, where.character}) + local out: Document.NodeInfo = { + type = node:type(), + source = node:source(), + parent_type = parent_node:type(), + parent_source = parent_node:source() + } - -- I don't know why we have to check character, character-1, and character+1 here but can confirm that we do - -- TODO - figure out why line_info and where.character are off by one sometimes - local type_id = line_info[where.character] or line_info[where.character-1] or line_info[where.character+1] + -- for completion + if node:type() == "." or node:type() == ":" then + -- considered an error and need to get the previous symbol + local prev = node:prev_sibling() + if prev then + out.preceded_by = prev:source() + else + parent_node = parent_node:prev_sibling() + if parent_node:child_count() > 0 then + -- no previous symbol, so get parent's previous sibling's last child + out.preceded_by = parent_node:child(parent_node:child_count()-1):source() + else + out.preceded_by = parent_node:source() + end + end - if type_id == nil then - type_id = self:_try_lookup_from_deref(where.line, where.character, line_info, tr) + -- for function signature + elseif node:type() == "(" then + if parent_node:type() == "arguments" then + self._tree_cursor:goto_parent() + local function_call = self._tree_cursor:current_node():child_by_field_name("called_object") + if function_call then + out.preceded_by = function_call:source() + end - if type_id == nil then - tracing.warning(_module_name, "Could not find type id for file {} at position {}, line info {}", {self._uri.path, where, line_info}) - return nil + elseif parent_node:type() == "ERROR" then + for child in parent_node:children() do + if child:name() == "index" then + out.preceded_by = child:source() + break + end + end + end end - end - - tracing.trace(_module_name, "Successfully found type id {}", {type_id}) - - local type_info = tr.types[type_id] - - if type_info == nil then - tracing.warning(_module_name, "Could not find type info for type id '{}'", {type_id}) - return nil - end - - tracing.trace(_module_name, "Successfully found type info: {}", {type_info}) - if type_info.str == "string" then - -- TODO - why is this necessary in order to get string deref vars? - tracing.trace(_module_name, "Hackily changed type info to string as a special case", {}) - return (self._server_state:get_env().globals["string"] as {string:tl.TypeInfo})["t"] - end - - local canonical_type_info = tr.types[type_info.ref] - - if canonical_type_info ~= nil then - tracing.trace(_module_name, "Successfully found type info from ref field: {}", {canonical_type_info}) - return canonical_type_info - end + if out.preceded_by == "self" or + out.source:find("self[%.%:]") or + out.parent_source:find("self[%.%:]") then + + while parent_node:type() ~= "program" do + self._tree_cursor:goto_parent() + parent_node = self._tree_cursor:current_node() + if parent_node:type() == "function_statement" then + local function_name = parent_node:child_by_field_name("name") + if function_name then + local base_name = function_name:child_by_field_name("base") + out.self_type = base_name:source() + break + end + elseif parent_node:type() == "ERROR" then + -- for some reason you can't get the node by name in an error state, but you can still iterate over to it + for child in parent_node:children() do + if child:name() == "function_name" then + out.self_type = child:child_by_field_name("base"):source() + break + end + end + end + end - return type_info -end + end -local function indent(n: integer): string - return (" "):rep(n) -end -local function ti(list: {string}, ...: string) - for i = 1, select("#", ...) do - table.insert(list, (select(i, ...))) - end -end + return out -function Document:show_type(info: tl.TypeInfo, depth?: integer): string - if not info then return "???" end - depth = depth or 1 - if depth > 4 then - return "..." end - local out : {string} = {} - - local function ins(...: string) - ti(out, ...) - end + local start_point = node:start_point() + local end_point = node:end_point() - local tr, _ = self:get_type_report() + while moved do + start_point = node:start_point() + end_point = node:end_point() - local function show_record_field(name: string, field_id: integer): string - local field = {} - ti(field, indent(depth)) - local field_type = tr.types[field_id] - if field_type.str:match("^type ") then - ti(field, "type ", name, " = ", (self:show_type(field_type, depth + 1):gsub("^type ", ""))) - else - ti(field, name, ": ", self:show_type(field_type, depth + 1)) - end - ti(field, "\n") - return table.concat(field) - end + 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) + end - local function show_record_fields(fields: {string:integer}) - if not fields then - ins("--???\n") - return - end - local f = {} - for name, field_id in pairs(fields) do - ti(f, show_record_field(name, field_id)) - end - local function get_name(s: string): string - return (s:match("^%s*type ([^=]+)") or s:match("^%s*([^:]+)")):lower() - end - table.sort(f, function(a: string, b: string): boolean - return get_name(a) < get_name(b) - end) - for _, field in ipairs(f) do - ins(field) + elseif y >= start_point.row and y <= end_point.row then + return self:_parser_token(y, x) end - end - if info.ref then - return info.str .. " => " .. self:show_type(tr.types[info.ref], depth + 1) - elseif info.str == "type record" or info.str == "record" then - ins(info.str) - if not info.fields then - ins(" ??? end") - return table.concat(out) - end - ins("\n") - show_record_fields(info.fields) - ins(indent(depth - 1)) - ins("end") - return table.concat(out) - elseif info.str == "type enum" then -- an enum def - ins("enum\n") - if info.enums then - for _, str in ipairs(info.enums) do - ins(indent(depth)) - ins(string.format("%q\n", str)) - end - else - ins(indent(depth)) - ins("--???") - ins("\n") - end - ins(indent(depth - 1)) - ins("end") - return table.concat(out) - else - return info.str + moved = self._tree_cursor:goto_next_sibling() + node = self._tree_cursor:current_node() end end -function Document:raw_token_at(where: lsp.Position): string - return get_raw_token_at(self:get_tokens(), where.line + 1, where.character + 1) -end - -function Document:token_at(where: lsp.Position): Token - return get_token_at(self:get_tokens(), where.line + 1, where.character + 1) +function Document:parser_token(y: integer, x: integer): Document.NodeInfo + self._tree_cursor:reset(self._tree:root()) + return self:_parser_token(y, x) end class.setup(Document, "Document", { diff --git a/src/teal_language_server/lsp.tl b/src/teal_language_server/lsp.tl index 2e7b1dc..38fdf1e 100644 --- a/src/teal_language_server/lsp.tl +++ b/src/teal_language_server/lsp.tl @@ -3,6 +3,8 @@ -- most of these are adapted from the typescript types the spec gives at -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/ +local tl = require("tl") + local record lsp enum ErrorName @@ -85,6 +87,7 @@ local record lsp "textDocument/definition" "textDocument/publishDiagnostics" "textDocument/completion" + "textDocument/signatureHelp" "shutdown" end @@ -118,6 +121,36 @@ local record lsp end completion_trigger_kind: {CompletionTriggerKind:integer} + enum CompletionItemKind + "Text" + "Method" + "Function" + "Constructor" + "Field" + "Variable" + "Class" + "Interface" + "Module" + "Property" + "Unit" + "Value" + "Enum" + "Keyword" + "Snippet" + "Color" + "File" + "Reference" + "Folder" + "EnumMember" + "Constant" + "Struct" + "Event" + "Operator" + "TypeParameter" + end + completion_item_kind: {CompletionItemKind:integer} + typecodes_to_kind: {integer:integer} + record CompletionContext triggerKind: integer triggerCharacter: string @@ -158,6 +191,66 @@ lsp.completion_trigger_kind = { TriggerForIncompleteCompletions = 3, } +lsp.completion_item_kind = { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} + +-- maybe could be moved to a different file or something. +-- it's one of the few that does a tl mapping to something outside of teal. +lsp.typecodes_to_kind = { + -- Lua types + [tl.typecodes.NIL] = lsp.completion_item_kind.Variable, + [tl.typecodes.NUMBER] = lsp.completion_item_kind.Variable, + [tl.typecodes.BOOLEAN] = lsp.completion_item_kind.Variable, + [tl.typecodes.STRING] = lsp.completion_item_kind.Variable, + [tl.typecodes.TABLE] = lsp.completion_item_kind.Struct, + [tl.typecodes.FUNCTION] = lsp.completion_item_kind.Function, + [tl.typecodes.USERDATA] = lsp.completion_item_kind.Variable, + [tl.typecodes.THREAD] = lsp.completion_item_kind.Variable, + -- -- Teal types + [tl.typecodes.INTEGER] = lsp.completion_item_kind.Variable, + [tl.typecodes.ENUM] = lsp.completion_item_kind.Enum, + [tl.typecodes.ARRAY] = lsp.completion_item_kind.Struct, + [tl.typecodes.RECORD] = lsp.completion_item_kind.Reference, + [tl.typecodes.MAP] = lsp.completion_item_kind.Struct, + [tl.typecodes.TUPLE] = lsp.completion_item_kind.Struct, + [tl.typecodes.INTERFACE] = lsp.completion_item_kind.Interface , + [tl.typecodes.SELF] = lsp.completion_item_kind.Struct, + [tl.typecodes.POLY] = lsp.completion_item_kind.Function, + [tl.typecodes.UNION] = lsp.completion_item_kind.TypeParameter, + -- -- Indirect types + [tl.typecodes.NOMINAL] = lsp.completion_item_kind.Variable, + [tl.typecodes.TYPE_VARIABLE] = lsp.completion_item_kind.Reference, + -- -- Special types + [tl.typecodes.ANY] = lsp.completion_item_kind.Variable, + [tl.typecodes.UNKNOWN] = lsp.completion_item_kind.Variable, + [tl.typecodes.INVALID] = lsp.completion_item_kind.Text, +} + function lsp.position(y: lsp.uinteger, x: lsp.uinteger): lsp.Position return { character = x, diff --git a/src/teal_language_server/lsp_formatter.tl b/src/teal_language_server/lsp_formatter.tl new file mode 100644 index 0000000..cdd5bc4 --- /dev/null +++ b/src/teal_language_server/lsp_formatter.tl @@ -0,0 +1,135 @@ +local tl = require("tl") + +local Document = require("teal_language_server.document") + +local record lsp_formatter + record Documentation + kind: string + value: string + end + + record SignatureHelp + record SignatureParameter + label: {integer, integer} + documentation: Documentation + end + + record Signature + label: string + parameters: {SignatureParameter} + documentation: Documentation + activeParameter: integer + end + + signatures: {Signature} + activeSignature: integer + activeParameter: integer + end +end + +local function _split_not_in_parenthesis(str: string, start: integer, finish: integer): {string} + local parens_count = 0 + local i = start + local output = {} + local start_field = i + while i <= finish do + if str:sub(i, i) == "(" then + parens_count = parens_count + 1 + end + if str:sub(i, i) == ")" then + parens_count = parens_count - 1 + end + if str:sub(i, i) == "," and parens_count == 0 then + output[#output + 1] = str:sub(start_field, i) + start_field = i + 2 + end + i = i + 1 + end + table.insert(output, str:sub(start_field, i)) + return output +end + +function lsp_formatter.create_function_string(type_string: string, arg_names: {string}, tk?: string): string + local _, _, types, args, returns = type_string:find("^function(.-)(%b())(.-)$") as (integer, integer, string, string, string) + local output: {string} = {} + if tk then output[1] = tk else output[1] = "function" end + output[2] = types + output[3] = "(" + + for i, argument in ipairs(_split_not_in_parenthesis(args, 2, #args-2)) do + output[#output+1] = arg_names[i] + output[#output+1] = ": " + output[#output+1] = argument + output[#output+1] = " " + end + output[#output] = ")" + output[#output+1] = returns + return table.concat(output) +end + +local record StringBuilder + strings: {string} + + build: function(self): string = macroexp(self: StringBuilder): string + return table.concat(self.strings, "\n") + end + + add: function(self, line: string): nil = macroexp(self: StringBuilder, line: string): nil + return table.insert(self.strings, line) + end +end + +function lsp_formatter.show_type(node_info: Document.NodeInfo, type_info: tl.TypeInfo, doc: Document): lsp_formatter.Documentation + local output: lsp_formatter.Documentation = {kind="markdown"} + local sb: StringBuilder = {strings = {}} + sb:add("```teal") + + if type_info.t == tl.typecodes.FUNCTION then + local args = doc:get_function_args_string(type_info) + if args ~= nil then + sb:add("function " .. lsp_formatter.create_function_string(type_info.str, args, node_info.source)) + else + sb:add(node_info.source .. ": " .. type_info.str) + end + + elseif type_info.t == tl.typecodes.POLY then + for i, type_ref in ipairs(type_info.types) do + local func_info = doc:resolve_type_ref(type_ref) + local args = doc:get_function_args_string(func_info) + if args ~= nil then + sb:add("function " .. lsp_formatter.create_function_string(func_info.str, args, node_info.source)) + else + local replaced_function = func_info.str:gsub("^function", node_info.source) + sb:add(replaced_function) + end + if i < #type_info.types then + sb:add("```") + sb:add("or") + sb:add("```teal") + end + end + + elseif type_info.t == tl.typecodes.ENUM then + sb:add("enum " .. type_info.str) + for _, _enum in ipairs(type_info.enums) do + sb:add(' "' .. _enum .. '"') + end + sb:add("end") + + elseif type_info.t == tl.typecodes.RECORD then + sb:add("record " .. type_info.str) + for key, type_ref in pairs(type_info.fields) do + local type_ref_info = doc:resolve_type_ref(type_ref) + sb:add(' ' .. key .. ': ' .. type_ref_info.str) + end + sb:add("end") + + else + sb:add(node_info.source .. ": " .. type_info.str) + end + sb:add("```") + output.value = sb:build() + return output +end + +return lsp_formatter diff --git a/src/teal_language_server/lsp_reader_writer.tl b/src/teal_language_server/lsp_reader_writer.tl index 6761860..8716ce0 100644 --- a/src/teal_language_server/lsp_reader_writer.tl +++ b/src/teal_language_server/lsp_reader_writer.tl @@ -2,7 +2,7 @@ local _module_name = "lsp_reader_writer" local StdinReader = require("teal_language_server.stdin_reader") local lsp = require("teal_language_server.lsp") -local json = require("dkjson") +local json = require("cjson") local uv = require("luv") local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") diff --git a/src/teal_language_server/misc_handlers.tl b/src/teal_language_server/misc_handlers.tl index 2fbd60d..a009542 100644 --- a/src/teal_language_server/misc_handlers.tl +++ b/src/teal_language_server/misc_handlers.tl @@ -5,6 +5,7 @@ local EnvUpdater = require("teal_language_server.env_updater") local args_parser = require("teal_language_server.args_parser") local TraceStream = require("teal_language_server.trace_stream") local DocumentManager = require("teal_language_server.document_manager") +local Document = require("teal_language_server.document") local ServerState = require("teal_language_server.server_state") local LspReaderWriter = require("teal_language_server.lsp_reader_writer") local Path = require("teal_language_server.path") @@ -15,6 +16,14 @@ local uv = require("luv") local asserts = require("teal_language_server.asserts") local tracing = require("teal_language_server.tracing") local class = require("teal_language_server.class") +local tl = require("tl") +local lsp_formatter = require("teal_language_server.lsp_formatter") + +local indexable_parent_types : {string:boolean} = { + ["index"] = true, + ["method_index"] = true, + ["function_name"] = true +} local record MiscHandlers _env_updater: EnvUpdater @@ -23,8 +32,8 @@ local record MiscHandlers _server_state: ServerState _lsp_reader_writer: LspReaderWriter _lsp_events_manager: LspEventsManager - _has_handled_initialize:boolean - _cl_args:args_parser.CommandLineArgs + _has_handled_initialize: boolean + _cl_args: args_parser.CommandLineArgs metamethod __call: function(self: MiscHandlers, lsp_events_manager:LspEventsManager, lsp_reader_writer: LspReaderWriter, server_state: ServerState, document_manager: DocumentManager, trace_stream: TraceStream, args:args_parser.CommandLineArgs, env_updater: EnvUpdater): MiscHandlers end @@ -40,12 +49,12 @@ function MiscHandlers:__init(lsp_events_manager:LspEventsManager, lsp_reader_wri self._trace_stream = trace_stream self._cl_args = args self._env_updater = env_updater + end function MiscHandlers:_on_initialize(params:lsp.Method.Params, id:integer):nil asserts.that(not self._has_handled_initialize) self._has_handled_initialize= true - local root_dir_str:string if params.rootUri then @@ -100,10 +109,12 @@ end function MiscHandlers:_on_did_save(params:lsp.Method.Params):nil local td = params.textDocument as lsp.TextDocument local doc = self._document_manager:get(Uri.parse(td.uri)) + if not doc then tracing.warning(_module_name, "Unable to find document: {}", {td.uri}) return end + doc:update_text(params.text as string, td.version) -- Don't bother calling process_and_publish_results here because this -- will happen anyway after the full env update @@ -114,7 +125,7 @@ end function MiscHandlers:_on_did_change(params:lsp.Method.Params):nil local td = params.textDocument as lsp.TextDocument - local doc = self._document_manager:get(Uri.parse(td.uri)) + local doc = self._document_manager:get(Uri.parse(td.uri)) if not doc then tracing.warning(_module_name, "Unable to find document: {}", {td.uri}) return @@ -124,15 +135,27 @@ function MiscHandlers:_on_did_change(params:lsp.Method.Params):nil doc:process_and_publish_results() end -function MiscHandlers:_on_completion(params:lsp.Method.Params, id:integer):nil +local function split_by_symbols(input: string, self_type: string, stop_at?: string): {string} + local t = {} + for str in string.gmatch(input, "([^%.%:]+)") do + if str == "self" then + table.insert(t, self_type) + else + table.insert(t, str) + end + if stop_at and stop_at == str then + break + end + end + return t +end + +function MiscHandlers:_get_node_info(params:lsp.Method.Params, pos: lsp.Position): Document.NodeInfo, Document local context = params.context as lsp.CompletionContext - -- triggerKind 1 = manual invocation (do we need to handle this?) - -- triggerKind 3 = "Completion was re-triggered as the current completion list is incomplete." - if context.triggerKind ~= 2 then + if context and context.triggerKind ~= lsp.completion_trigger_kind.TriggerCharacter then tracing.warning(_module_name, "Ignoring completion request given kind: {}", {context.triggerKind}) - self._lsp_reader_writer:send_rpc(id, nil) - return + return nil end local td = params.textDocument as lsp.TextDocument @@ -140,60 +163,123 @@ function MiscHandlers:_on_completion(params:lsp.Method.Params, id:integer):nil if not doc then tracing.warning(_module_name, "No doc found for completion request", {}) - self._lsp_reader_writer:send_rpc(id, nil) - return + return nil end - local pos = params.position as lsp.Position - pos.character = pos.character - 2 + tracing.warning(_module_name, "Received request for completion at position: {}", {pos}) + local node_info = doc:parser_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 + end + tracing.warning(_module_name, "Found node info at pos", node_info) + return node_info, doc +end - tracing.info(_module_name, "Received request for completion at position: {}", {pos}) +function MiscHandlers:_on_completion(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, nil) + return + end - local tk = doc:token_at(pos) + tracing.warning(_module_name, "Got nodeinfo: {}", node_info) + + local tks: {string} + + -- literally at the . or : and are trying to autocomplete + -- preceded_by should have everything we need in it. + if node_info.type == "." or node_info.type == ":" then + tks = split_by_symbols(node_info.preceded_by, node_info.self_type) + tracing.warning(_module_name, "Received request for completion at character: {}", {tks}) + + -- completion request in the middle of a word like in "node_info.par" while typing "parent_source" + elseif node_info.type == "identifier" then + -- the types that we would need to index into to get type info from + 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 - if not tk then - tracing.warning(_module_name, "Could not find token at given position", {}) + -- takes it back to the just after the first dot, then does completion + -- if there's only one symbol, will return an empty list and type_information_for_tokens will just + -- show what's in scope + tks[#tks] = nil + + -- trying to define something new, probably don't want the in-scope thing to popup + -- we could probably help complete simple types + if node_info.parent_type == "var" or + node_info.parent_type == "simple_type" or + node_info.parent_type == "table_type" then + self._lsp_reader_writer:send_rpc(id, nil) + return + end + + else self._lsp_reader_writer:send_rpc(id, nil) return end - local token_pos = lsp.position(tk.y, tk.x) - tracing.trace(_module_name, "Found actual token {} at position: {}", {tk.tk, token_pos}) - - local type_info = doc:type_information_at(token_pos) local items:{any} = { } + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) if not type_info then - tracing.trace(_module_name, "No type information found at calculated token position {}. Attempting to get type information by raw token instead.", {token_pos}) - - type_info = doc:type_information_for_token(tk) - - if not type_info then - tracing.warning(_module_name, "Also failed to find type type_info based on token", {}) - end + tracing.warning(_module_name, "Also failed to find type type_info based on token", {}) end if type_info then - tracing.trace(_module_name, "Successfully found type type_info '{}'", {type_info}) + tracing.warning(_module_name, "Successfully found type type_info '{}'", type_info) + local tr = doc:get_type_report() if type_info.ref then - local tr, _ = doc:get_type_report() - local real_type_info = tr.types[type_info.ref] - if real_type_info.fields then - for key, _ in pairs(real_type_info.fields) do - table.insert(items, {label = key}) + type_info = doc:resolve_type_ref(type_info.ref) + end + + -- string + if type_info.t == tl.typecodes.STRING then + type_info = tr.types[tr.globals["string"]] + end + + -- this seemed to be the best way to compare the first arg with itself when comparing ":", + -- sometimes the types come in as nominal and don't resolve all the way back, so the name is + -- what seems to work the most reliably + local original_str = type_info.str + + if type_info.fields then + for key, v in pairs(type_info.fields) do + type_info = doc:resolve_type_ref(v) + -- self based access should only show functions + if node_info.type == ":" then + if type_info.t == tl.typecodes.FUNCTION then + -- local args = doc:get_function_args_string(type_info) + if type_info.args and #type_info.args >= 1 then + local first_arg_type = doc:resolve_type_ref(type_info.args[1][1]) + if first_arg_type.t == tl.typecodes.SELF or (first_arg_type.t == tl.typecodes.NOMINAL and first_arg_type.str == original_str) then + tracing.warning(_module_name, "adding " .. key, {}) + table.insert(items, {label = key, kind = lsp.typecodes_to_kind[type_info.t]}) + else + tracing.warning(_module_name, "type info str: " .. original_str .. "first arg str: " .. first_arg_type.str, {}) + end + end + end + else + table.insert(items, {label = key, kind = lsp.typecodes_to_kind[type_info.t]}) end - else - tracing.warning(_module_name, "Unable to get fields for ref type", {}) end - else - if type_info.fields then - for key, _ in pairs(type_info.fields) do - table.insert(items, {label = key}) + + -- at a table, we might be able to help resolve the keys! + elseif type_info.keys then + type_info = doc:resolve_type_ref(type_info.keys) + + if type_info.enums then + for _, enum_value in ipairs(type_info.enums) do + table.insert(items, {label = enum_value, kind = lsp.typecodes_to_kind[type_info.t]}) end - else - tracing.warning(_module_name, "Unable to get fields for type", {}) end + else + tracing.warning(_module_name, "Unable to get fields for ref type", {}) end end @@ -201,50 +287,114 @@ function MiscHandlers:_on_completion(params:lsp.Method.Params, id:integer):nil table.insert(items, {label = "(none)"}) end + tracing.warning(_module_name, "Sending " .. #items .. " back to client", {}) + self._lsp_reader_writer:send_rpc(id, { isIncomplete = false, items = items }) end -function MiscHandlers:_on_definition(params:lsp.Method.Params, id:integer):nil - local td = params.textDocument as lsp.TextDocument - local doc = self._document_manager:get(Uri.parse(td.uri)) +function MiscHandlers:_on_signature_help(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, nil) + return + end - if not doc then - tracing.trace(_module_name, "[on_definition] No document found for given uri", {}) + local output: lsp_formatter.SignatureHelp = {} + tracing.warning(_module_name, "Got nodeinfo: {}", node_info) + + local tks: {string} + + if node_info.type == "(" then + tks = split_by_symbols(node_info.preceded_by, node_info.self_type) + tracing.warning(_module_name, "Received request for signature help at character: {}", {tks}) + else + self._lsp_reader_writer:send_rpc(id, nil) + return + end + + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) + + if type_info == nil then + self._lsp_reader_writer:send_rpc(id, nil) return end + output.signatures = {} + if type_info.t == tl.typecodes.POLY then + for _, type_ref in ipairs(type_info.types) do + type_info = doc:resolve_type_ref(type_ref) + local args = doc:get_function_args_string(type_info) + if args ~= nil then + local func_str = lsp_formatter.create_function_string(type_info.str, args, node_info.preceded_by) + table.insert(output.signatures, {label = func_str}) + + else + table.insert(output.signatures, {label = type_info.str}) + end + end + else + local args = doc:get_function_args_string(type_info) + if args ~= nil then + local func_str = lsp_formatter.create_function_string(type_info.str, args, node_info.preceded_by) + table.insert(output.signatures, {label = func_str}) + else + table.insert(output.signatures, {label = type_info.str}) + end + end + + tracing.warning(_module_name, "[_on_signature_help] Found type info: {}", {type_info}) + + self._lsp_reader_writer:send_rpc(id, output) +end + +function MiscHandlers:_on_definition(params:lsp.Method.Params, id:integer):nil local pos = params.position as lsp.Position - local tk = doc:token_at(pos) - if not tk then - tracing.trace(_module_name, "[on_definition] No token found at given position", {}) + local node_info, doc = self:_get_node_info(params, pos) + if node_info == nil then self._lsp_reader_writer:send_rpc(id, nil) return end - local token_pos = lsp.position(tk.y, tk.x) - local info = doc:type_information_at(token_pos) + tracing.trace(_module_name, "Received request for hover at position: {}", {pos}) - if not info or info.file == nil then + local tks: {string} = {} + if node_info.type == "identifier" then + -- parent types that show up with multiparts + if indexable_parent_types[node_info.parent_type] then + tks = split_by_symbols(node_info.parent_source, node_info.self_type, node_info.source) + else + tks = split_by_symbols(node_info.source, node_info.self_type) + end + else + tracing.warning(_module_name, "Can't hover over anything that isn't an identifier atm" .. node_info.type, {}) self._lsp_reader_writer:send_rpc(id, nil) return end - tracing.trace(_module_name, "[on_definition] Found type info: {}", {info}) + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) + + if not type_info or type_info.file == nil then + self._lsp_reader_writer:send_rpc(id, nil) + return + end + + tracing.trace(_module_name, "[on_definition] Found type type_info: {}", {type_info}) local file_uri: Uri - if #info.file == 0 then + if #type_info.file == 0 then file_uri = doc.uri else - local full_path:string + local full_path: string - if Path(info.file):is_absolute() then - full_path = info.file + if Path(type_info.file):is_absolute() then + full_path = type_info.file else - full_path = self._server_state.teal_project_root_dir.value .. "/" .. info.file + full_path = self._server_state.teal_project_root_dir.value .. "/" .. type_info.file end local file_path = Path(full_path) @@ -254,65 +404,74 @@ function MiscHandlers:_on_definition(params:lsp.Method.Params, id:integer):nil self._lsp_reader_writer:send_rpc(id, { uri = Uri.tostring(file_uri), range = { - start = lsp.position(info.y - 1, info.x - 1), - ["end"] = lsp.position(info.y - 1, info.x - 1), + start = lsp.position(type_info.y - 1, type_info.x - 1), + ["end"] = lsp.position(type_info.y - 1, type_info.x - 1), }, }) end function MiscHandlers:_on_hover(params:lsp.Method.Params, id:integer):nil - local td = params.textDocument as lsp.TextDocument - local doc = self._document_manager:get(Uri.parse(td.uri)) - - if not doc then - tracing.warning(_module_name, "Failed to find document for given params", {}) - self._lsp_reader_writer:send_rpc(id, 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 - local pos = params.position as lsp.Position tracing.trace(_module_name, "Received request for hover at position: {}", {pos}) - local tk = doc:token_at(pos) - if not tk then - tracing.warning(_module_name, "Could not find token at given position", {}) + local tks: {string} = {} + if node_info.type == "identifier" then + -- parent types that show up with multiparts + if indexable_parent_types[node_info.parent_type] then + tks = split_by_symbols(node_info.parent_source, node_info.self_type, node_info.source) + else + tks = split_by_symbols(node_info.source, node_info.self_type) + end + else + tracing.warning(_module_name, "Can't hover over anything that isn't an identifier atm" .. node_info.type, {}) self._lsp_reader_writer:send_rpc(id, { - contents = { " No info found " } + contents = { node_info.parent_type, ":", node_info.type }, + range = { + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character + #node_info.source), + }, }) return end - local token_pos = lsp.position(tk.y, tk.x) - tracing.trace(_module_name, "Found actual token '{}' at position: '{}'", {tk.tk, token_pos}) - local type_info = doc:type_information_at(token_pos) + + local type_info = doc:type_information_for_tokens(tks, pos.line, pos.character) + if not type_info then - tracing.warning(_module_name, "No type information found at calculated token position. Attempting to get type information by raw token instead...", {}) - - type_info = doc:type_information_for_token(tk) - - if not type_info then - tracing.warning(_module_name, "Also failed to find type info based on token", {}) - self._lsp_reader_writer:send_rpc(id, { - contents = { tk.tk .. ":", " No type_info found " }, - range = { - start = lsp.position(token_pos.line, token_pos.character), - ["end"] = lsp.position(token_pos.line, token_pos.character + #tk.tk), - }, - }) - return - end + tracing.warning(_module_name, "Also failed to find type info based on token", {}) + self._lsp_reader_writer:send_rpc(id, { + contents = { node_info.source .. ":", " No type_info found " }, + range = { + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character + #node_info.source), + }, + }) + return end - tracing.trace(_module_name, "Successfully found type_info: {}", {type_info}) + tracing.warning(_module_name, "Successfully found type_info: {}", {type_info}) - local type_str = doc:show_type(type_info) + local type_str = lsp_formatter.show_type(node_info, type_info, doc) self._lsp_reader_writer:send_rpc(id, { - contents = { tk.tk .. ":", type_str }, + contents = type_str, range = { - start = lsp.position(token_pos.line, token_pos.character), - ["end"] = lsp.position(token_pos.line, token_pos.character + #tk.tk), + start = lsp.position(pos.line, pos.character), + ["end"] = lsp.position(pos.line, pos.character + #node_info.source), }, }) end + function MiscHandlers:_add_handler(name:lsp.Method.Name, handler:function(MiscHandlers, lsp.Method.Params, integer):nil) self._lsp_events_manager:set_handler(name, function(params:lsp.Method.Params, id:integer) handler(self, params, id) end) end @@ -325,8 +484,11 @@ function MiscHandlers:initialize() 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:_add_handler("textDocument/definition", self._on_definition) + self:_add_handler("textDocument/signatureHelp", self._on_signature_help) self:_add_handler("textDocument/hover", self._on_hover) + + -- planning to figure out what defintion/declaration vs type definition/declaration should go to + self:_add_handler("textDocument/definition", self._on_definition) end class.setup(MiscHandlers, "MiscHandlers", { diff --git a/src/teal_language_server/server_state.tl b/src/teal_language_server/server_state.tl index 440e109..4db7b22 100644 --- a/src/teal_language_server/server_state.tl +++ b/src/teal_language_server/server_state.tl @@ -40,10 +40,12 @@ local capabilities = { }, hoverProvider = true, definitionProvider = true, - -- typeDefinitionProvider = true, completionProvider = { triggerCharacters = { ".", ":" }, }, + signatureHelpProvider = { + triggerCharacters = { "(" } + } } function ServerState:_validate_config(c:TealProjectConfig) @@ -200,7 +202,9 @@ end function ServerState:_load_config(root_dir:Path):TealProjectConfig local config_path = root_dir:join("tlconfig.lua") - asserts.that(config_path:exists()) + if config_path:exists() == false then + return {} as TealProjectConfig + end local success, result = pcall(dofile, config_path.value) diff --git a/src/teal_language_server/trace_stream.tl b/src/teal_language_server/trace_stream.tl index 0b0fe17..03c90dc 100644 --- a/src/teal_language_server/trace_stream.tl +++ b/src/teal_language_server/trace_stream.tl @@ -3,7 +3,7 @@ local util = require("teal_language_server.util") local asserts = require("teal_language_server.asserts") local Path = require("teal_language_server.path") local TraceEntry = require("teal_language_server.trace_entry") -local json = require("dkjson") +local json = require("cjson") local uv = require("luv") local class = require("teal_language_server.class") local inspect = require("inspect") diff --git a/teal-language-server-0.0.5-1.rockspec b/teal-language-server-0.0.5-1.rockspec index 2b11096..a65565a 100644 --- a/teal-language-server-0.0.5-1.rockspec +++ b/teal-language-server-0.0.5-1.rockspec @@ -14,7 +14,7 @@ description = { dependencies = { "luafilesystem", "tl", - "dkjson", + "lua-cjson", "argparse", "inspect", "luv", diff --git a/types/cjson.d.tl b/types/cjson.d.tl new file mode 100644 index 0000000..aca7662 --- /dev/null +++ b/types/cjson.d.tl @@ -0,0 +1,11 @@ + +local record cjson + record Null + userdata + end + encode: function({string:any}): string + decode: function(string): {string:any} + null: Null +end + +return cjson diff --git a/types/dkjson.d.tl b/types/dkjson.d.tl deleted file mode 100644 index a7c7638..0000000 --- a/types/dkjson.d.tl +++ /dev/null @@ -1,33 +0,0 @@ - ---[[ -- local type json = {string:json} -]] - -local record dkjson - record JsonState - indent: boolean - keyorder: {string} - level: number - buffer: {string} - bufferlen: number - tables: {table:boolean} - exception: function(string, string, string, string): boolean|string, string - end - encode: function({string:any}, ?JsonState): string - - decode: function(string, ?number, ?any, ?table): {string:any} - - null: table - - version: string - - quotestring: function(string): string - - addnewline: function(JsonState) - - encodeexception: function(string, any, JsonState, string): string - - use_lpeg: function(): dkjson -end - -return dkjson diff --git a/types/ltreesitter.d.tl b/types/ltreesitter.d.tl new file mode 100644 index 0000000..f1391eb --- /dev/null +++ b/types/ltreesitter.d.tl @@ -0,0 +1,111 @@ +-- Autogenerated type definitions + +local record ltreesitter + -- Exports + + record Cursor userdata + current_field_name: function(Cursor): string -- csrc/tree_cursor.c:32 + current_node: function(Cursor): Node -- csrc/tree_cursor.c:20 + goto_first_child: function(Cursor): boolean -- csrc/tree_cursor.c:74 + goto_first_child_for_byte: function(Cursor, integer): integer -- csrc/tree_cursor.c:83 + goto_next_sibling: function(Cursor): boolean -- csrc/tree_cursor.c:65 + goto_parent: function(Cursor): boolean -- csrc/tree_cursor.c:56 + reset: function(Cursor, Node) -- csrc/tree_cursor.c:46 + end + record Node userdata + child: function(Node, idx: integer): Node -- csrc/node.c:129 + child_by_field_name: function(Node, string): Node -- csrc/node.c:339 + child_count: function(Node): integer -- csrc/node.c:146 + children: function(Node): function(): Node -- csrc/node.c:218 + create_cursor: function(Node): Cursor -- csrc/node.c:372 + end_byte: function(Node): integer -- csrc/node.c:39 + end_point: function(Node): Point -- csrc/node.c:72 + is_extra: function(Node): boolean -- csrc/node.c:106 + is_missing: function(Node): boolean -- csrc/node.c:97 + is_named: function(Node): boolean -- csrc/node.c:88 + name: function(Node): string -- csrc/node.c:319 + named_child: function(Node, idx: integer): Node -- csrc/node.c:155 + named_child_count: function(Node): integer -- csrc/node.c:170 + named_children: function(Node): function(): Node -- csrc/node.c:232 + next_named_sibling: function(Node): Node -- csrc/node.c:274 + next_sibling: function(Node): Node -- csrc/node.c:244 + prev_named_sibling: function(Node): Node -- csrc/node.c:289 + prev_sibling: function(Node): Node -- csrc/node.c:259 + source: function(Node): string -- csrc/node.c:357 + start_byte: function(Node): integer -- csrc/node.c:30 + start_point: function(Node): Point -- csrc/node.c:55 + type: function(Node): string -- csrc/node.c:21 + end + record Parser userdata + get_ranges: function(Parser): {Range} -- csrc/parser.c:633 + get_version: function(Parser): integer -- csrc/parser.c:682 + parse_string: function(Parser, string, ? Tree): Tree -- csrc/parser.c:377 + parse_with: function(Parser, reader: function(integer, Point): (string), old_tree: Tree): Tree -- csrc/parser.c:470 + query: function(Parser, string): Query -- csrc/parser.c:650 + set_ranges: function(Parser, {Range}): boolean -- csrc/parser.c:554 + set_timeout: function(Parser, integer) -- csrc/parser.c:520 + end + record Query userdata + capture: function(Query, Node, start: integer | Point, end_: integer | Point): function(): (Node, string) -- csrc/query.c:514 + exec: function(Query, Node, start: integer | Point, end_: integer | Point) -- csrc/query.c:602 + match: function(Query, Node, start: integer | Point, end_: integer | Point): function(): Match -- csrc/query.c:468 + source: function(Query): string -- csrc/query.c:657 + with: function(Query, {string:function(...: string | Node | {Node}): any...}): Query -- csrc/query.c:543 + end + record Tree userdata + copy: function(Tree): Tree -- csrc/tree.c:90 + edit: function( + Tree, + start_byte: integer, + old_end_byte: integer, + new_end_byte: integer, + start_point_row: integer, + start_point_col: integer, + old_end_point_row: integer, + old_end_point_col: integer, + new_end_point_row: integer, + new_end_point_col: integer + ) -- csrc/tree.c:177 + edit_s: function(Tree, TreeEdit) -- csrc/tree.c:120 + get_changed_ranges: function(old: Tree, new: Tree): {Range} -- csrc/tree.c:206 + root: function(Tree): Node -- csrc/tree.c:71 + end + load: function(file_name: string, language_name: string): Parser, string -- csrc/parser.c:111 + require: function(library_file_name: string, language_name: string): Parser -- csrc/parser.c:167 + version: string -- csrc/ltreesitter.c:14 + + -- Inlines + + record TreeEdit + start_byte: integer + old_end_byte: integer + new_end_byte: integer + + start_point: Point + old_end_point: Point + new_end_point: Point + end -- csrc/tree.c:109 + + record Match + id: integer + pattern_index: integer + capture_count: integer + captures: {string:Node|{Node}} + end -- csrc/query.c:328 + + record Point + row: integer + column: integer + end -- csrc/node.c:48 + + record Range + start_byte: integer + end_byte: integer + + start_point: Point + end_point: Point + end -- csrc/parser.c:531 +end + +return ltreesitter +