├── .gitattributes ├── .github └── workflows │ ├── create-binaries.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── teal-language-server ├── gen └── teal_language_server │ ├── args_parser.lua │ ├── asserts.lua │ ├── class.lua │ ├── constants.lua │ ├── document.lua │ ├── document_manager.lua │ ├── env_updater.lua │ ├── lsp.lua │ ├── lsp_events_manager.lua │ ├── lsp_formatter.lua │ ├── lsp_reader_writer.lua │ ├── main.lua │ ├── misc_handlers.lua │ ├── path.lua │ ├── server_state.lua │ ├── stdin_reader.lua │ ├── teal_project_config.lua │ ├── trace_entry.lua │ ├── trace_stream.lua │ ├── tracing.lua │ ├── tracing_util.lua │ ├── uri.lua │ └── util.lua ├── luarocks.lock ├── scripts ├── create_binary.sh ├── create_windows_binary.sh ├── generate_lua.bat ├── generate_lua.sh ├── lint_teal.bat ├── lint_teal.sh ├── run_all.bat ├── run_tests.sh ├── setup_local_luarocks.bat └── setup_local_luarocks.sh ├── spec └── document_tree_sitter_spec.lua ├── src └── teal_language_server │ ├── args_parser.tl │ ├── asserts.tl │ ├── class.d.tl │ ├── class.lua │ ├── constants.tl │ ├── document.tl │ ├── document_manager.tl │ ├── env_updater.tl │ ├── lsp.tl │ ├── lsp_events_manager.tl │ ├── lsp_formatter.tl │ ├── lsp_reader_writer.tl │ ├── main.tl │ ├── misc_handlers.tl │ ├── path.tl │ ├── server_state.tl │ ├── stdin_reader.tl │ ├── teal_project_config.tl │ ├── trace_entry.tl │ ├── trace_stream.tl │ ├── tracing.tl │ ├── tracing_util.tl │ ├── uri.tl │ └── util.tl ├── teal-language-server-0.1.1-1.rockspec ├── tlconfig.lua └── types ├── argparse.d.tl ├── cjson.d.tl ├── inspect.d.tl ├── lfs.d.tl ├── ltreesitter.d.tl ├── lusc.d.tl ├── luv.d.tl └── tl.d.tl /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tl linguist-language=Lua 2 | build/** linguist-generated 3 | -------------------------------------------------------------------------------- /.github/workflows/create-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Create Binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | pull_request: 8 | 9 | jobs: 10 | macos: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [macos, macos-arm64] 15 | include: 16 | - os: macos 17 | runner: macos-13 18 | - os: macos-arm64 19 | runner: macos-latest 20 | name: ${{ matrix.os }} 21 | runs-on: ${{ matrix.runner }} 22 | steps: 23 | # Checks-out the repository under $GITHUB_WORKSPACE. 24 | - uses: actions/checkout@v4 25 | - name: Build teal-language-server (${{ matrix.os }}) 26 | shell: bash 27 | run: | 28 | ./scripts/create_binary.sh 29 | - uses: actions/upload-artifact@v4 30 | if: always() 31 | with: 32 | name: tls-${{matrix.os}} 33 | path: "./tls" 34 | 35 | windows: 36 | strategy: 37 | fail-fast: false 38 | runs-on: windows-2022 39 | steps: 40 | # waiting on new luarocks release for fix, so we don't need to install via choco 41 | - name: install tree-sitter-cli 42 | run: | 43 | choco install tree-sitter --version 0.24.4 44 | - name: verify tree-sitter-cli is installed on path 45 | run: tree-sitter --version 46 | 47 | # Checks-out the repository under $GITHUB_WORKSPACE. 48 | - uses: actions/checkout@v4 49 | - uses: ilammy/msvc-dev-cmd@v1 50 | - uses: leafo/gh-actions-lua@v11 51 | - uses: luarocks/gh-actions-luarocks@v5 52 | 53 | - name: luarocks workaround for tree-sitter 54 | run: luarocks config --lua-version 5.4 rocks_provided.tree-sitter-cli 0.24.4-2 55 | 56 | - name: Build teal-language-server 57 | run: | 58 | luarocks init --tree=.\luarocks 59 | luarocks make --tree=.\luarocks 60 | 61 | - name: Run create_binary script 62 | shell: bash 63 | run: ./scripts/create_windows_binary.sh 64 | 65 | - uses: actions/upload-artifact@v4 66 | if: always() 67 | with: 68 | name: tls-windows 69 | path: "teal-language-server" 70 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | 2 | name: test 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | luaVersion: ["5.1", "5.2", "5.3", "5.4" ] 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@main 17 | 18 | - name: Install Lua 19 | uses: leafo/gh-actions-lua@v10 20 | with: 21 | luaVersion: ${{ matrix.luaVersion }} 22 | 23 | - name: Install LuaRocks 24 | uses: leafo/gh-actions-luarocks@v4 25 | with: 26 | luarocksVersion: "3.10.0" 27 | 28 | - name: Build 29 | run: scripts/setup_local_luarocks.sh 30 | 31 | - name: Run Tests 32 | run: scripts/run_tests.sh 33 | 34 | - name: Lint 35 | run: scripts/lint_teal.sh 36 | 37 | - name: Check for untracked or modified files 38 | run: | 39 | if [ -n "$(git status --porcelain)" ]; then 40 | echo "Error: Untracked or modified files found." 41 | git status 42 | exit 1 43 | else 44 | echo "No untracked or modified files." 45 | fi 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /luarocks 2 | /lua 3 | /lua_modules 4 | /.luarocks 5 | /luarocks.bat 6 | /lua.bat 7 | /luarocks_tree 8 | /*.src.rock 9 | 10 | # install dirs on CI machine 11 | /.install 12 | /.lua 13 | 14 | **.so 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Steve Vermeulen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Teal Language Server 3 | 4 | A language server for the [Teal language](https://github.com/teal-language/tl) 5 | 6 | [![test](https://github.com/teal-language/teal-language-server/actions/workflows/test.yml/badge.svg)](https://github.com/teal-language/teal-language-server/actions/workflows/test.yml) 7 | 8 | # Installation 9 | 10 | ### From luarocks 11 | 12 | * `luarocks install teal-language-server` 13 | * `teal-language-server` 14 | 15 | Tested on Windows, Linux and macOS 16 | 17 | ### From source 18 | 19 | * Clone repo 20 | * From repo root: 21 | * `scripts/setup_local_luarocks` 22 | * `./lua_modules/bin/teal-language-server` 23 | 24 | # Features 25 | 26 | * Go to definition (`textDocument/definition`) 27 | * Linting (`textDocument/publishDiagnostics`) 28 | * Intellisense (`textDocument/completion`) 29 | * Hover (`textDocument/hover`) 30 | 31 | # Editor Setup 32 | 33 | ### Neovim 34 | 35 | Install the [lspconfig plugin](https://github.com/neovim/nvim-lspconfig) and put the following in your `init.vim` or `init.lua` 36 | 37 | ```lua 38 | local lspconfig = require("lspconfig") 39 | 40 | lspconfig.teal_ls.setup {} 41 | ``` 42 | 43 | # Usage 44 | 45 | ``` 46 | teal-language-server [--verbose=true] [--log-mode=none|by_proj_path|by_date] 47 | ``` 48 | 49 | Note: 50 | 51 | * All args are optional 52 | * By default, logging is 'none' which disables logging completely 53 | * When logging is set to by_proj_path or by_date, the log is output to `[User Home Directory]/.cache/teal-language-server` 54 | 55 | -------------------------------------------------------------------------------- /bin/teal-language-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | require('teal_language_server.main') 3 | -------------------------------------------------------------------------------- /gen/teal_language_server/args_parser.lua: -------------------------------------------------------------------------------- 1 | 2 | local asserts = require("teal_language_server.asserts") 3 | 4 | local args_parser = { CommandLineArgs = {} } 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | function args_parser.parse_args() 18 | local argparse = require("argparse") 19 | local parser = argparse("teal-language-server", "Teal Language Server") 20 | 21 | parser:option("-V --verbose", "") 22 | 23 | parser:option("-L --log-mode", "Specify approach to logging. By default it is none which means no logging. by_date names the file according to date. by_proj_path names file according to the teal project path"): 24 | choices({ "none", "by_date", "by_proj_path" }) 25 | 26 | local raw_args = parser:parse() 27 | 28 | local verbose = raw_args["verbose"] 29 | local log_mode = raw_args["log_mode"] 30 | 31 | if log_mode == nil then 32 | log_mode = "none" 33 | else 34 | asserts.that(log_mode == "by_date" or log_mode == "by_proj_path") 35 | end 36 | 37 | local args = { 38 | verbose = verbose, 39 | log_mode = log_mode, 40 | } 41 | 42 | return args 43 | end 44 | 45 | return args_parser 46 | -------------------------------------------------------------------------------- /gen/teal_language_server/asserts.lua: -------------------------------------------------------------------------------- 1 | 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 string = _tl_compat and _tl_compat.string or string 2 | local asserts = {} 3 | 4 | 5 | local function _raise(format, ...) 6 | if format == nil then 7 | error("Assert hit!") 8 | else 9 | 10 | if format:find("%%s") ~= nil then 11 | error("Unexpected assert string - should use {} instead of %s") 12 | end 13 | format = format:gsub("{}", "%%s") 14 | error(string.format(format, ...)) 15 | end 16 | end 17 | 18 | function asserts.fail(format, ...) 19 | _raise(format, ...) 20 | end 21 | function asserts.that(condition, format, ...) 22 | if not condition then 23 | _raise(format, ...) 24 | end 25 | end 26 | 27 | function asserts.is_nil(value, format, ...) 28 | if value ~= nil then 29 | if format == nil then 30 | _raise("Expected nil value but instead found '{}'", value) 31 | else 32 | _raise(format, ...) 33 | end 34 | end 35 | end 36 | 37 | function asserts.is_not_nil(value, format, ...) 38 | if value == nil then 39 | if format == nil then 40 | _raise("Expected non-nil value") 41 | else 42 | _raise(format, ...) 43 | end 44 | end 45 | end 46 | 47 | return asserts 48 | -------------------------------------------------------------------------------- /gen/teal_language_server/class.lua: -------------------------------------------------------------------------------- 1 | 2 | local asserts = require("teal_language_server.asserts") 3 | 4 | local Class = {} 5 | 6 | function Class.try_get_name(cls) 7 | return cls.__name 8 | end 9 | 10 | function Class.get_name(cls) 11 | local name = Class.try_get_name(cls) 12 | if name == nil then 13 | error("Attempted to get class name for non-class type!") 14 | end 15 | return name 16 | end 17 | 18 | function Class.try_get_class_name_for_instance(instance) 19 | if instance == nil or type(instance) ~= "table" then 20 | return nil 21 | end 22 | local class = instance.__class 23 | if class == nil then 24 | return nil 25 | end 26 | return Class.try_get_name(class) 27 | end 28 | 29 | function Class.try_get_class_for_instance(obj) 30 | return obj.__class 31 | end 32 | 33 | function Class.get_class_for_instance(obj) 34 | local cls = Class.try_get_class_for_instance(obj) 35 | if cls == nil then 36 | error("Attempted to get class for non-class type!") 37 | end 38 | return cls 39 | end 40 | 41 | function Class.is_instance(obj, cls) 42 | return obj.__class == cls 43 | end 44 | 45 | function Class.get_class_name_for_instance(instance) 46 | local name = Class.try_get_class_name_for_instance(instance) 47 | if name == nil then 48 | error("Attempted to get class name for non-class type!") 49 | end 50 | return name 51 | end 52 | 53 | function Class.setup(class, class_name, options) 54 | class.__name = class_name 55 | 56 | options = options or {} 57 | 58 | -- This is useful sometimes to verify that a given table represents a class 59 | class.__is_class = true 60 | 61 | if options.attributes ~= nil then 62 | class._attributes = options.attributes 63 | end 64 | 65 | if options.interfaces ~= nil then 66 | class._interfaces = options.interfaces 67 | end 68 | 69 | if options.getters then 70 | for k, v in pairs(options.getters) do 71 | if type(v) == "string" then 72 | asserts.that(class[v] ~= nil, "Found getter property '{}' mapped to non-existent method '{}' for class '{}'", k, v, class_name) 73 | end 74 | end 75 | end 76 | 77 | local nilable_members = {} 78 | 79 | if options.nilable_members ~= nil then 80 | for _, value in ipairs(options.nilable_members) do 81 | nilable_members[value] = true 82 | end 83 | end 84 | 85 | if options.setters then 86 | for k, v in pairs(options.setters) do 87 | if type(v) == "string" then 88 | asserts.that(class[v] ~= nil, "Found setter property '{}' mapped to non-existent method '{}' for class '{}'", k, v, class_name) 89 | end 90 | end 91 | end 92 | 93 | -- Assume closed by default 94 | local is_closed = true 95 | 96 | if options.closed ~= nil and not options.closed then 97 | is_closed = false 98 | end 99 | 100 | local is_immutable = false 101 | 102 | if options.immutable ~= nil and options.immutable then 103 | is_immutable = true 104 | end 105 | 106 | if is_immutable then 107 | asserts.that(is_closed, "Attempted to create a non-closed immutable class '{}'. This is not allowed", class_name) 108 | end 109 | 110 | local function create_immutable_wrapper(t, class_name) 111 | local proxy = {} 112 | local mt = { 113 | __index = t, 114 | __newindex = function(t, k, v) 115 | asserts.fail("Attempted to change field '{}' of immutable class '{}'", k, class_name) 116 | end, 117 | __len = function() 118 | return #t 119 | end, 120 | __pairs = function() 121 | return pairs(t) 122 | end, 123 | __ipairs = function() 124 | return ipairs(t) 125 | end, 126 | __tostring = function() 127 | return tostring(t) 128 | end 129 | } 130 | setmetatable(proxy, mt) 131 | return proxy 132 | end 133 | 134 | setmetatable( 135 | class, { 136 | __call = function(_, ...) 137 | local mt = {} 138 | local instance = setmetatable({ __class = class }, mt) 139 | 140 | -- We need to call __init before defining __newindex below 141 | -- This is also nice because all classes are required to define 142 | -- default values for all their members in __init 143 | if class.__init ~= nil then 144 | class.__init(instance, ...) 145 | end 146 | 147 | local tostring_handler = class["__tostring"] 148 | if tostring_handler ~= nil then 149 | mt.__tostring = tostring_handler 150 | end 151 | 152 | mt.__index = function(_, k) 153 | if options.getters then 154 | local getter_value = options.getters[k] 155 | if getter_value then 156 | if type(getter_value) == "string" then 157 | return class[getter_value](instance) 158 | end 159 | 160 | return getter_value(instance) 161 | end 162 | end 163 | 164 | local static_member = class[k] 165 | if is_closed then 166 | -- This check means that member values cannot ever be set to nil 167 | -- So we provide the closed flag to allow for this case 168 | asserts.that(static_member ~= nil or nilable_members[k] ~= nil, "Attempted to get non-existent member '{}' on class '{}'. If its valid for the class to have nil members, then pass 'closed=false' to class.setup", k, class_name) 169 | end 170 | return static_member 171 | end 172 | 173 | mt.__newindex = function(_, k, value) 174 | if is_closed and nilable_members[k] == nil then 175 | asserts.that(options.setters, "Attempted to set non-existent property '{}' on class '{}'", k, class_name) 176 | 177 | local setter_value = options.setters[k] 178 | asserts.that(setter_value, "Attempted to set non-existent property '{}' on class '{}'", k, class_name) 179 | 180 | if type(setter_value) == "string" then 181 | rawget(class, setter_value)(instance, value) 182 | else 183 | setter_value(instance, value) 184 | end 185 | else 186 | asserts.that(not is_immutable) 187 | rawset(instance, k, value) 188 | end 189 | end 190 | 191 | if is_immutable then 192 | return create_immutable_wrapper(instance, class_name) 193 | end 194 | 195 | return instance 196 | end, 197 | } 198 | ) 199 | end 200 | 201 | return Class 202 | -------------------------------------------------------------------------------- /gen/teal_language_server/constants.lua: -------------------------------------------------------------------------------- 1 | 2 | local constants = {} 3 | 4 | 5 | return constants 6 | -------------------------------------------------------------------------------- /gen/teal_language_server/document_manager.lua: -------------------------------------------------------------------------------- 1 | local _module_name = "document_manager" 2 | 3 | 4 | local ServerState = require("teal_language_server.server_state") 5 | local LspReaderWriter = require("teal_language_server.lsp_reader_writer") 6 | local Uri = require("teal_language_server.uri") 7 | local Document = require("teal_language_server.document") 8 | local asserts = require("teal_language_server.asserts") 9 | local class = require("teal_language_server.class") 10 | 11 | local DocumentManager = {} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | function DocumentManager:__init(lsp_reader_writer, server_state) 22 | asserts.is_not_nil(lsp_reader_writer) 23 | asserts.is_not_nil(server_state) 24 | 25 | self._docs = {} 26 | self._lsp_reader_writer = lsp_reader_writer 27 | self._server_state = server_state 28 | end 29 | 30 | function DocumentManager:open(uri, content, version) 31 | asserts.that(self._docs[uri.path] == nil) 32 | local doc = Document(uri, content, version, self._lsp_reader_writer, self._server_state) 33 | self._docs[uri.path] = doc 34 | return doc 35 | end 36 | 37 | function DocumentManager:close(uri) 38 | asserts.that(self._docs[uri.path] ~= nil) 39 | self._docs[uri.path] = nil 40 | end 41 | 42 | function DocumentManager:get(uri) 43 | return self._docs[uri.path] 44 | end 45 | 46 | class.setup(DocumentManager, "DocumentManager", { 47 | getters = { 48 | docs = function(self) 49 | return self._docs 50 | end, 51 | }, 52 | }) 53 | return DocumentManager 54 | -------------------------------------------------------------------------------- /gen/teal_language_server/env_updater.lua: -------------------------------------------------------------------------------- 1 | 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 package = _tl_compat and _tl_compat.package or package; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local _module_name = "env_updater" 2 | 3 | 4 | local DocumentManager = require("teal_language_server.document_manager") 5 | local lusc = require("lusc") 6 | local ServerState = require("teal_language_server.server_state") 7 | local tl = require("tl") 8 | local TealProjectConfig = require("teal_language_server.teal_project_config") 9 | local uv = require("luv") 10 | local asserts = require("teal_language_server.asserts") 11 | local tracing = require("teal_language_server.tracing") 12 | local class = require("teal_language_server.class") 13 | 14 | local init_path = package.path 15 | local init_cpath = package.cpath 16 | 17 | local EnvUpdater = {} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | function EnvUpdater:__init(server_state, root_nursery, document_manager) 28 | asserts.is_not_nil(document_manager) 29 | 30 | self._change_detected = lusc.new_sticky_event() 31 | self._server_state = server_state 32 | self._substitutions = {} 33 | self._root_nursery = root_nursery 34 | self._document_manager = document_manager 35 | end 36 | 37 | function EnvUpdater:_init_env_from_config(cfg) 38 | local function ivalues(t) 39 | local i = 0 40 | return function() 41 | i = i + 1 42 | return t[i] 43 | end 44 | end 45 | 46 | local path_separator = package.config:sub(1, 1) 47 | local shared_lib_ext = package.cpath:match("(%.%w+)%s*$") or ".so" 48 | 49 | local function prepend_to_lua_path(path_str) 50 | if path_str:sub(-1) == path_separator then 51 | path_str = path_str:sub(1, -2) 52 | end 53 | 54 | path_str = path_str .. path_separator 55 | 56 | package.path = path_str .. "?.lua;" .. 57 | path_str .. "?" .. path_separator .. "init.lua;" .. 58 | package.path 59 | 60 | package.cpath = path_str .. "?." .. shared_lib_ext .. ";" .. 61 | package.cpath 62 | end 63 | 64 | local function esc_char(c) 65 | return "%" .. c 66 | end 67 | 68 | local function str_esc(s, sub) 69 | return s:gsub( 70 | "[%^%$%(%)%%%.%[%]%*%+%-%?]", 71 | sub or 72 | esc_char) 73 | 74 | end 75 | 76 | local function add_module_substitute(source_dir, mod_name) 77 | self._substitutions[source_dir] = "^" .. str_esc(mod_name) 78 | end 79 | 80 | local function init_teal_env(gen_compat, gen_target, env_def) 81 | local opts = { 82 | defaults = { 83 | gen_compat = gen_compat, 84 | gen_target = gen_target, 85 | }, 86 | predefined_modules = { env_def }, 87 | } 88 | 89 | local env = tl.new_env(opts) 90 | env.report_types = true 91 | return env 92 | end 93 | 94 | cfg = cfg or {} 95 | 96 | for dir in ivalues(cfg.include_dir or {}) do 97 | prepend_to_lua_path(dir) 98 | end 99 | 100 | if cfg.source_dir and cfg.module_name then 101 | add_module_substitute(cfg.source_dir, cfg.module_name) 102 | end 103 | 104 | local env, err = init_teal_env(cfg.gen_compat, cfg.gen_target, cfg.global_env_def) 105 | if not env then 106 | return nil, err 107 | end 108 | 109 | return env 110 | end 111 | 112 | function EnvUpdater:_generate_env() 113 | local config = self._server_state.config 114 | asserts.is_not_nil(config) 115 | 116 | 117 | 118 | package.path = init_path 119 | package.cpath = init_cpath 120 | 121 | local env, errs = self:_init_env_from_config(config) 122 | 123 | if errs ~= nil and #errs > 0 then 124 | tracing.debug(_module_name, "Loaded env with errors:\n{}", { errs }) 125 | end 126 | 127 | return env 128 | end 129 | 130 | function EnvUpdater:_update_env_on_changes() 131 | local required_delay_without_saves_sec = 0.1 132 | 133 | while true do 134 | self._change_detected:await() 135 | self._change_detected:unset() 136 | 137 | 138 | 139 | 140 | while true do 141 | lusc.await_sleep(required_delay_without_saves_sec) 142 | if self._change_detected.is_set then 143 | tracing.debug(_module_name, "Detected consecutive change events, waiting again...", {}) 144 | self._change_detected:unset() 145 | else 146 | tracing.debug(_module_name, "Successfully waited for buffer time. Now updating env...", {}) 147 | break 148 | end 149 | end 150 | 151 | tracing.debug(_module_name, "Now updating env...", {}) 152 | local start_time = uv.hrtime() 153 | local env = self:_generate_env() 154 | self._server_state:set_env(env) 155 | local elapsed_time_ms = (uv.hrtime() - start_time) / 1e6 156 | tracing.debug(_module_name, "Completed env update in {} ms", { elapsed_time_ms }) 157 | 158 | for _, doc in pairs(self._document_manager.docs) do 159 | doc:clear_cache() 160 | doc:process_and_publish_results() 161 | end 162 | end 163 | end 164 | 165 | function EnvUpdater:schedule_env_update() 166 | self._change_detected:set() 167 | end 168 | 169 | function EnvUpdater:initialize() 170 | local env = self:_generate_env() 171 | self._server_state:set_env(env) 172 | 173 | self._root_nursery:start_soon(function() 174 | self:_update_env_on_changes() 175 | end) 176 | end 177 | 178 | class.setup(EnvUpdater, "EnvUpdater") 179 | return EnvUpdater 180 | -------------------------------------------------------------------------------- /gen/teal_language_server/lsp.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | local tl = require("tl") 7 | 8 | local lsp = { Message = { ResponseError = {} }, Position = {}, Range = {}, Location = {}, Diagnostic = {}, Method = {}, TextDocument = {}, TextDocumentContentChangeEvent = {}, CompletionContext = {} } 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | lsp.error_code = { 162 | InternalError = -32603, 163 | InvalidParams = -32602, 164 | InvalidRequest = -32600, 165 | MethodNotFound = -32601, 166 | ParseError = -32700, 167 | ServerNotInitialized = -32002, 168 | UnknownErrorCode = -32001, 169 | serverErrorEnd = -32000, 170 | serverErrorStart = -32099, 171 | 172 | RequestCancelled = -32800, 173 | } 174 | 175 | lsp.severity = { 176 | Error = 1, 177 | Warning = 2, 178 | Information = 3, 179 | Hint = 4, 180 | } 181 | 182 | lsp.sync_kind = { 183 | None = 0, 184 | Full = 1, 185 | Incremental = 2, 186 | } 187 | 188 | lsp.completion_trigger_kind = { 189 | Invoked = 1, 190 | TriggerCharacter = 2, 191 | TriggerForIncompleteCompletions = 3, 192 | } 193 | 194 | lsp.completion_item_kind = { 195 | Text = 1, 196 | Method = 2, 197 | Function = 3, 198 | Constructor = 4, 199 | Field = 5, 200 | Variable = 6, 201 | Class = 7, 202 | Interface = 8, 203 | Module = 9, 204 | Property = 10, 205 | Unit = 11, 206 | Value = 12, 207 | Enum = 13, 208 | Keyword = 14, 209 | Snippet = 15, 210 | Color = 16, 211 | File = 17, 212 | Reference = 18, 213 | Folder = 19, 214 | EnumMember = 20, 215 | Constant = 21, 216 | Struct = 22, 217 | Event = 23, 218 | Operator = 24, 219 | TypeParameter = 25, 220 | } 221 | 222 | 223 | 224 | lsp.typecodes_to_kind = { 225 | 226 | [tl.typecodes.NIL] = lsp.completion_item_kind.Variable, 227 | [tl.typecodes.NUMBER] = lsp.completion_item_kind.Variable, 228 | [tl.typecodes.BOOLEAN] = lsp.completion_item_kind.Variable, 229 | [tl.typecodes.STRING] = lsp.completion_item_kind.Variable, 230 | [tl.typecodes.TABLE] = lsp.completion_item_kind.Struct, 231 | [tl.typecodes.FUNCTION] = lsp.completion_item_kind.Function, 232 | [tl.typecodes.USERDATA] = lsp.completion_item_kind.Variable, 233 | [tl.typecodes.THREAD] = lsp.completion_item_kind.Variable, 234 | 235 | [tl.typecodes.INTEGER] = lsp.completion_item_kind.Variable, 236 | [tl.typecodes.ENUM] = lsp.completion_item_kind.Enum, 237 | [tl.typecodes.ARRAY] = lsp.completion_item_kind.Struct, 238 | [tl.typecodes.RECORD] = lsp.completion_item_kind.Reference, 239 | [tl.typecodes.MAP] = lsp.completion_item_kind.Struct, 240 | [tl.typecodes.TUPLE] = lsp.completion_item_kind.Struct, 241 | [tl.typecodes.INTERFACE] = lsp.completion_item_kind.Interface, 242 | [tl.typecodes.SELF] = lsp.completion_item_kind.Struct, 243 | [tl.typecodes.POLY] = lsp.completion_item_kind.Function, 244 | [tl.typecodes.UNION] = lsp.completion_item_kind.TypeParameter, 245 | 246 | [tl.typecodes.NOMINAL] = lsp.completion_item_kind.Variable, 247 | [tl.typecodes.TYPE_VARIABLE] = lsp.completion_item_kind.Reference, 248 | 249 | [tl.typecodes.ANY] = lsp.completion_item_kind.Variable, 250 | [tl.typecodes.UNKNOWN] = lsp.completion_item_kind.Variable, 251 | [tl.typecodes.INVALID] = lsp.completion_item_kind.Text, 252 | } 253 | 254 | function lsp.position(y, x) 255 | return { 256 | character = x, 257 | line = y, 258 | } 259 | end 260 | 261 | return lsp 262 | -------------------------------------------------------------------------------- /gen/teal_language_server/lsp_events_manager.lua: -------------------------------------------------------------------------------- 1 | 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 debug = _tl_compat and _tl_compat.debug or debug; local xpcall = _tl_compat and _tl_compat.xpcall or xpcall; local _module_name = "lsp_events_manager" 2 | 3 | local lsp = require("teal_language_server.lsp") 4 | local LspReaderWriter = require("teal_language_server.lsp_reader_writer") 5 | local lusc = require("lusc") 6 | local asserts = require("teal_language_server.asserts") 7 | local tracing = require("teal_language_server.tracing") 8 | local class = require("teal_language_server.class") 9 | 10 | local LspEventsManager = {} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | function LspEventsManager:__init(root_nursery, lsp_reader_writer) 20 | asserts.is_not_nil(root_nursery) 21 | asserts.is_not_nil(lsp_reader_writer) 22 | 23 | self._handlers = {} 24 | self._lsp_reader_writer = lsp_reader_writer 25 | self._root_nursery = root_nursery 26 | end 27 | 28 | function LspEventsManager:set_handler(method, handler) 29 | asserts.that(self._handlers[method] == nil) 30 | self._handlers[method] = handler 31 | end 32 | 33 | function LspEventsManager:_trigger(method, params, id) 34 | tracing.info(_module_name, "Received request from client for method {}", { method }) 35 | 36 | if self._handlers[method] then 37 | local ok 38 | local err 39 | 40 | ok, err = xpcall( 41 | function() self._handlers[method](params, id) end, 42 | debug.traceback) 43 | 44 | if ok then 45 | tracing.debug(_module_name, "Successfully handled request with method {}", { method }) 46 | else 47 | tracing.error(_module_name, "Error in handler for request with method {}: {}", { method, err }) 48 | end 49 | else 50 | tracing.warning(_module_name, "No handler found for event with method {}", { method }) 51 | end 52 | end 53 | 54 | function LspEventsManager:_receive_initialize_request() 55 | local initialize_data = self._lsp_reader_writer:receive_rpc() 56 | 57 | asserts.is_not_nil(initialize_data) 58 | 59 | asserts.that(initialize_data.method ~= nil, "No method in initial request") 60 | asserts.that(initialize_data.method == "initialize", "Initial method was not 'initialize'") 61 | 62 | tracing.trace(_module_name, "Received initialize request from client with data: {}", { initialize_data }) 63 | 64 | self:_trigger( 65 | "initialize", initialize_data.params, initialize_data.id) 66 | end 67 | 68 | function LspEventsManager:initialize() 69 | self._root_nursery:start_soon(function() 70 | 71 | self:_receive_initialize_request() 72 | 73 | while true do 74 | local data = self._lsp_reader_writer:receive_rpc() 75 | asserts.is_not_nil(data) 76 | asserts.is_not_nil(data.method) 77 | 78 | self:_trigger( 79 | data.method, data.params, data.id) 80 | end 81 | end) 82 | end 83 | 84 | class.setup(LspEventsManager, "LspEventsManager") 85 | return LspEventsManager 86 | -------------------------------------------------------------------------------- /gen/teal_language_server/lsp_formatter.lua: -------------------------------------------------------------------------------- 1 | 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") 2 | 3 | local Document = require("teal_language_server.document") 4 | 5 | local lsp_formatter = { Documentation = {}, SignatureHelp = { SignatureParameter = {}, Signature = {} } } 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | local function _split_not_in_parenthesis(str, start, finish) 31 | local parens_count = 0 32 | local i = start 33 | local output = {} 34 | local start_field = i 35 | while i <= finish do 36 | if str:sub(i, i) == "(" then 37 | parens_count = parens_count + 1 38 | end 39 | if str:sub(i, i) == ")" then 40 | parens_count = parens_count - 1 41 | end 42 | if str:sub(i, i) == "," and parens_count == 0 then 43 | output[#output + 1] = str:sub(start_field, i) 44 | start_field = i + 2 45 | end 46 | i = i + 1 47 | end 48 | table.insert(output, str:sub(start_field, i)) 49 | return output 50 | end 51 | 52 | function lsp_formatter.create_function_string(type_string, arg_names, tk) 53 | local _, _, types, args, returns = type_string:find("^function(.-)(%b())(.-)$") 54 | local output = {} 55 | if tk then output[1] = tk else output[1] = "function" end 56 | output[2] = types 57 | output[3] = "(" 58 | 59 | for i, argument in ipairs(_split_not_in_parenthesis(args, 2, #args - 2)) do 60 | output[#output + 1] = arg_names[i] 61 | output[#output + 1] = ": " 62 | output[#output + 1] = argument 63 | output[#output + 1] = " " 64 | end 65 | output[#output] = ")" 66 | output[#output + 1] = returns 67 | return table.concat(output) 68 | end 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | function lsp_formatter.show_type(node_info, type_info, doc) 83 | local output = { kind = "markdown" } 84 | local sb = { strings = {} } 85 | table.insert(sb.strings, "```teal") 86 | 87 | if type_info.t == tl.typecodes.FUNCTION then 88 | local args = doc:get_function_args_string(type_info) 89 | if args ~= nil then 90 | table.insert(sb.strings, "function " .. lsp_formatter.create_function_string(type_info.str, args, node_info.source)) 91 | else 92 | table.insert(sb.strings, node_info.source .. ": " .. type_info.str) 93 | end 94 | 95 | elseif type_info.t == tl.typecodes.POLY then 96 | for i, type_ref in ipairs(type_info.types) do 97 | local func_info = doc:resolve_type_ref(type_ref) 98 | local args = doc:get_function_args_string(func_info) 99 | if args ~= nil then 100 | table.insert(sb.strings, "function " .. lsp_formatter.create_function_string(func_info.str, args, node_info.source)) 101 | else 102 | local replaced_function = func_info.str:gsub("^function", node_info.source) 103 | table.insert(sb.strings, replaced_function) 104 | end 105 | if i < #type_info.types then 106 | table.insert(sb.strings, "```") 107 | table.insert(sb.strings, "or") 108 | table.insert(sb.strings, "```teal") 109 | end 110 | end 111 | 112 | elseif type_info.t == tl.typecodes.ENUM then 113 | table.insert(sb.strings, "enum " .. type_info.str) 114 | for _, _enum in ipairs(type_info.enums) do 115 | table.insert(sb.strings, ' "' .. _enum .. '"') 116 | end 117 | table.insert(sb.strings, "end") 118 | 119 | elseif type_info.t == tl.typecodes.RECORD then 120 | table.insert(sb.strings, "record " .. type_info.str) 121 | for key, type_ref in pairs(type_info.fields) do 122 | local type_ref_info = doc:resolve_type_ref(type_ref) 123 | table.insert(sb.strings, ' ' .. key .. ': ' .. type_ref_info.str) 124 | end 125 | table.insert(sb.strings, "end") 126 | 127 | else 128 | table.insert(sb.strings, node_info.source .. ": " .. type_info.str) 129 | end 130 | table.insert(sb.strings, "```") 131 | output.value = table.concat(sb.strings, "\n") 132 | return output 133 | end 134 | 135 | return lsp_formatter 136 | -------------------------------------------------------------------------------- /gen/teal_language_server/lsp_reader_writer.lua: -------------------------------------------------------------------------------- 1 | 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 string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local _module_name = "lsp_reader_writer" 2 | 3 | local StdinReader = require("teal_language_server.stdin_reader") 4 | local lsp = require("teal_language_server.lsp") 5 | local json = require("cjson") 6 | local uv = require("luv") 7 | local asserts = require("teal_language_server.asserts") 8 | local tracing = require("teal_language_server.tracing") 9 | local class = require("teal_language_server.class") 10 | 11 | local LspReaderWriter = {} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | function LspReaderWriter:__init(stdin_reader) 20 | asserts.is_not_nil(stdin_reader) 21 | self._stdin_reader = stdin_reader 22 | self._disposed = false 23 | end 24 | 25 | 26 | 27 | 28 | 29 | 30 | local function json_nullable(x) 31 | if x == nil then 32 | return json.null 33 | end 34 | return x 35 | end 36 | 37 | local contenttype = { 38 | ["application/vscode-jsonrpc; charset=utf8"] = true, 39 | ["application/vscode-jsonrpc; charset=utf-8"] = true, 40 | } 41 | 42 | function LspReaderWriter:_parse_header(lines) 43 | local len 44 | local content_type 45 | 46 | for _, line in ipairs(lines) do 47 | local key, val = line:match("^([^:]+): (.+)$") 48 | 49 | asserts.that(key ~= nil and val ~= nil, "invalid header: " .. line) 50 | 51 | tracing.trace(_module_name, "Request Header: {}: {}", { key, val }) 52 | 53 | if key == "Content-Length" then 54 | asserts.is_nil(len) 55 | len = tonumber(val) 56 | elseif key == "Content-Type" then 57 | if contenttype[val] == nil then 58 | asserts.fail("Invalid Content-Type '{}'", val) 59 | end 60 | asserts.is_nil(content_type) 61 | content_type = val 62 | else 63 | asserts.fail("Unexpected header: {}", line) 64 | end 65 | end 66 | 67 | asserts.that(len ~= nil, "Missing Content-Length") 68 | 69 | return { 70 | length = len, 71 | content_type = content_type, 72 | } 73 | end 74 | 75 | function LspReaderWriter:initialize() 76 | self._stdout = uv.new_pipe(false) 77 | asserts.that(self._stdout ~= nil) 78 | assert(self._stdout:open(1)) 79 | tracing.trace(_module_name, "Opened pipe for stdout") 80 | end 81 | 82 | function LspReaderWriter:dispose() 83 | asserts.that(not self._disposed) 84 | self._disposed = true 85 | self._stdout:close() 86 | tracing.debug(_module_name, "Closed pipe for stdout") 87 | end 88 | 89 | function LspReaderWriter:_decode_header() 90 | local header_lines = {} 91 | 92 | tracing.trace(_module_name, "Reading LSP rpc header...") 93 | while true do 94 | local header_line = self._stdin_reader:read_line() 95 | 96 | if #header_line == 0 then 97 | break 98 | end 99 | 100 | table.insert(header_lines, header_line) 101 | end 102 | 103 | return self:_parse_header(header_lines) 104 | end 105 | 106 | function LspReaderWriter:receive_rpc() 107 | local header_info = self:_decode_header() 108 | 109 | tracing.trace(_module_name, "Successfully read LSP rpc header: {}\nWaiting to receive body...", { header_info }) 110 | local body_line = self._stdin_reader:read(header_info.length) 111 | tracing.trace(_module_name, "Received request Body: {}", { body_line }) 112 | 113 | local data = json.decode(body_line) 114 | 115 | asserts.that(data and type(data) == 'table', "Malformed json") 116 | asserts.that(data.jsonrpc == "2.0", "Incorrect jsonrpc version! Got {} but expected 2.0", data.jsonrpc) 117 | 118 | return data 119 | end 120 | 121 | function LspReaderWriter:_encode(t) 122 | assert(t.jsonrpc == "2.0", "Expected jsonrpc to be 2.0") 123 | 124 | local msg = json.encode(t) 125 | 126 | local content = "Content-Length: " .. tostring(#msg) .. "\r\n\r\n" .. msg 127 | assert(self._stdout:write(content)) 128 | 129 | tracing.trace(_module_name, "Sending data: {}", { content }) 130 | end 131 | 132 | function LspReaderWriter:send_rpc(id, t) 133 | self:_encode({ 134 | jsonrpc = "2.0", 135 | id = json_nullable(id), 136 | result = json_nullable(t), 137 | }) 138 | end 139 | 140 | function LspReaderWriter:send_rpc_error(id, name, msg, data) 141 | self:_encode({ 142 | jsonrpc = "2.0", 143 | id = json_nullable(id), 144 | error = { 145 | code = lsp.error_code[name] or lsp.error_code.UnknownErrorCode, 146 | message = msg, 147 | data = data, 148 | }, 149 | }) 150 | end 151 | 152 | function LspReaderWriter:send_rpc_notification(method, params) 153 | self:_encode({ 154 | jsonrpc = "2.0", 155 | method = method, 156 | params = params, 157 | }) 158 | end 159 | 160 | class.setup(LspReaderWriter, "LspReaderWriter", { 161 | nilable_members = { '_stdout' }, 162 | }) 163 | return LspReaderWriter 164 | -------------------------------------------------------------------------------- /gen/teal_language_server/main.lua: -------------------------------------------------------------------------------- 1 | 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 table = _tl_compat and _tl_compat.table or table; local _module_name = "main" 2 | 3 | 4 | local EnvUpdater = require("teal_language_server.env_updater") 5 | local DocumentManager = require("teal_language_server.document_manager") 6 | local ServerState = require("teal_language_server.server_state") 7 | local LspEventsManager = require("teal_language_server.lsp_events_manager") 8 | local lusc = require("lusc") 9 | local uv = require("luv") 10 | local TraceStream = require("teal_language_server.trace_stream") 11 | local args_parser = require("teal_language_server.args_parser") 12 | local MiscHandlers = require("teal_language_server.misc_handlers") 13 | local StdinReader = require("teal_language_server.stdin_reader") 14 | local LspReaderWriter = require("teal_language_server.lsp_reader_writer") 15 | local tracing = require("teal_language_server.tracing") 16 | local util = require("teal_language_server.util") 17 | local TraceEntry = require("teal_language_server.trace_entry") 18 | 19 | 20 | 21 | 22 | 23 | local function init_logging(verbose) 24 | local trace_stream = TraceStream() 25 | trace_stream:initialize() 26 | 27 | tracing.add_stream(function(entry) 28 | trace_stream:log_entry(entry) 29 | end) 30 | 31 | if verbose then 32 | tracing.set_min_level("TRACE") 33 | else 34 | tracing.set_min_level("INFO") 35 | end 36 | return trace_stream 37 | end 38 | 39 | local function main() 40 | 41 | 42 | local cached_entries = {} 43 | tracing.add_stream(function(entry) 44 | if cached_entries then 45 | table.insert(cached_entries, entry) 46 | end 47 | end) 48 | 49 | local args = args_parser.parse_args() 50 | 51 | local trace_stream 52 | 53 | if args.log_mode ~= "none" then 54 | trace_stream = init_logging(args.verbose) 55 | 56 | for _, entry in ipairs(cached_entries) do 57 | trace_stream:log_entry(entry) 58 | end 59 | 60 | 61 | 62 | 63 | 64 | end 65 | 66 | cached_entries = nil 67 | 68 | tracing.info(_module_name, "Started new instance teal-language-server. Lua Version: {}. Platform: {}", { _VERSION, util.get_platform() }) 69 | tracing.info(_module_name, "Received command line args: {}", { args }) 70 | tracing.info(_module_name, "CWD = {}", { uv.cwd() }) 71 | 72 | local disposables 73 | 74 | local function initialize() 75 | tracing.debug(_module_name, "Running object graph construction phase...", {}) 76 | 77 | local root_nursery = lusc.get_root_nursery() 78 | local stdin_reader = StdinReader() 79 | local lsp_reader_writer = LspReaderWriter(stdin_reader) 80 | local lsp_events_manager = LspEventsManager(root_nursery, lsp_reader_writer) 81 | local server_state = ServerState() 82 | local document_manager = DocumentManager(lsp_reader_writer, server_state) 83 | local env_updater = EnvUpdater(server_state, root_nursery, document_manager) 84 | local misc_handlers = MiscHandlers(lsp_events_manager, lsp_reader_writer, server_state, document_manager, trace_stream, args, env_updater) 85 | 86 | tracing.debug(_module_name, "Running initialize phase...", {}) 87 | stdin_reader:initialize() 88 | lsp_reader_writer:initialize() 89 | lsp_events_manager:initialize() 90 | misc_handlers:initialize() 91 | 92 | lsp_events_manager:set_handler("shutdown", function() 93 | tracing.info(_module_name, "Received shutdown request from client. Cancelling all lusc tasks...", {}) 94 | root_nursery.cancel_scope:cancel() 95 | end) 96 | 97 | disposables = { 98 | stdin_reader, lsp_reader_writer, 99 | } 100 | end 101 | 102 | local function dispose() 103 | tracing.info(_module_name, "Disposing...", {}) 104 | 105 | if disposables then 106 | for _, disposable in ipairs(disposables) do 107 | disposable:dispose() 108 | end 109 | end 110 | end 111 | 112 | local lusc_timer = uv.new_timer() 113 | lusc_timer:start(0, 0, function() 114 | tracing.trace(_module_name, "Received entry point call from luv") 115 | 116 | lusc.start({ 117 | 118 | generate_debug_names = true, 119 | on_completed = function(err) 120 | if err ~= nil then 121 | tracing.error(_module_name, "Received on_completed request with error:\n{}", { err }) 122 | else 123 | tracing.info(_module_name, "Received on_completed request") 124 | end 125 | 126 | dispose() 127 | end, 128 | }) 129 | 130 | lusc.schedule(function() 131 | tracing.trace(_module_name, "Received entry point call from lusc luv") 132 | initialize() 133 | end) 134 | 135 | 136 | lusc.stop() 137 | end) 138 | 139 | local function run_luv() 140 | tracing.trace(_module_name, "Running luv event loop...") 141 | uv.run() 142 | tracing.trace(_module_name, "Luv event loop stopped") 143 | lusc_timer:close() 144 | 145 | uv.walk(function(handle) 146 | if not handle:is_closing() then 147 | local handle_type = handle:get_type() 148 | tracing.warning(_module_name, "Found unclosed handle of type '{}', closing it.", { handle_type }) 149 | handle:close() 150 | end 151 | end) 152 | 153 | uv.run('nowait') 154 | 155 | if uv.loop_close() then 156 | tracing.info(_module_name, "luv event loop closed gracefully") 157 | else 158 | tracing.warning(_module_name, "Could not close luv event loop gracefully") 159 | end 160 | end 161 | 162 | util.try({ 163 | action = run_luv, 164 | catch = function(err) 165 | tracing.error(_module_name, "Error: {}", { err }) 166 | error(err) 167 | end, 168 | }) 169 | end 170 | 171 | main() 172 | -------------------------------------------------------------------------------- /gen/teal_language_server/path.lua: -------------------------------------------------------------------------------- 1 | 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 pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local type = type; local _module_name = "path" 2 | 3 | local asserts = require("teal_language_server.asserts") 4 | local class = require("teal_language_server.class") 5 | local util = require("teal_language_server.util") 6 | local tracing = require("teal_language_server.tracing") 7 | local uv = require("luv") 8 | 9 | local default_dir_permissions = tonumber('755', 8) 10 | 11 | local Path = { WriteTextOpts = {}, CreateDirectoryArgs = {} } 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | function Path:__init(value) 30 | asserts.that(type(value) == "string") 31 | asserts.that(#value > 0, "Path must be non empty string") 32 | 33 | self._value = value 34 | end 35 | 36 | function Path:is_valid() 37 | local result = self._value:find("[" .. util.string_escape_special_chars("<>\"|?*") .. "]") 38 | return result == nil 39 | end 40 | 41 | function Path:get_value() 42 | return self._value 43 | end 44 | 45 | function Path:__tostring() 46 | 47 | return self._value 48 | end 49 | 50 | local function get_path_separator() 51 | if util.get_platform() == "windows" then 52 | return "\\" 53 | end 54 | 55 | return "/" 56 | end 57 | 58 | local function _join(left, right) 59 | if right == "." then 60 | return left 61 | end 62 | 63 | local combinedPath = left 64 | local lastCharLeft = left:sub(-1) 65 | 66 | local firstCharRight = right:sub(1, 1) 67 | local hasBeginningSlash = firstCharRight == '/' or firstCharRight == '\\' 68 | 69 | if lastCharLeft ~= '/' and lastCharLeft ~= '\\' then 70 | if not hasBeginningSlash then 71 | combinedPath = combinedPath .. get_path_separator() 72 | end 73 | else 74 | if hasBeginningSlash then 75 | right = right:sub(2, #right) 76 | end 77 | end 78 | 79 | combinedPath = combinedPath .. right 80 | return combinedPath 81 | end 82 | 83 | function Path:join(...) 84 | local args = { ... } 85 | local result = self._value 86 | 87 | for _, value in ipairs(args) do 88 | result = _join(result, value) 89 | end 90 | 91 | return Path(result) 92 | end 93 | 94 | function Path:is_absolute() 95 | if util.get_platform() == "windows" then 96 | return self._value:match('^[a-zA-Z]:[\\/]') ~= nil 97 | end 98 | 99 | return util.string_starts_with(self._value, '/') 100 | end 101 | 102 | function Path:is_relative() 103 | return not self:is_absolute() 104 | end 105 | 106 | local function _remove_trailing_seperator_if_exists(path) 107 | local result = path:match('^(.*[^\\/])[\\/]*$') 108 | asserts.is_not_nil(result, "Failed when processing path '{}'", path) 109 | return result 110 | end 111 | 112 | local function array_from_iterator(itr) 113 | local result = {} 114 | for value in itr do 115 | table.insert(result, value) 116 | end 117 | return result 118 | end 119 | 120 | function Path:get_parts() 121 | if self._value == '/' then 122 | return {} 123 | end 124 | 125 | 126 | local fixed_value = _remove_trailing_seperator_if_exists(self._value) 127 | return array_from_iterator(string.gmatch(fixed_value, "([^\\/]+)")) 128 | end 129 | 130 | function Path:try_get_parent() 131 | if self._value == '/' then 132 | return nil 133 | end 134 | 135 | 136 | local temp_path = _remove_trailing_seperator_if_exists(self._value) 137 | 138 | 139 | 140 | if not temp_path:match('[\\/]') then 141 | return nil 142 | end 143 | 144 | 145 | 146 | local parent_path_str = temp_path:match('^(.*)[\\/][^\\/]*$') 147 | 148 | if util.get_platform() ~= 'windows' and #parent_path_str == 0 then 149 | parent_path_str = "/" 150 | end 151 | 152 | return Path(parent_path_str) 153 | end 154 | 155 | function Path:get_parent() 156 | local result = self:try_get_parent() 157 | asserts.is_not_nil(result, "Expected to find parent but none was found for path '{}'", self._value) 158 | return result 159 | end 160 | 161 | function Path:get_parents() 162 | local result = {} 163 | local parent = self:try_get_parent() 164 | if not parent then 165 | return result 166 | end 167 | table.insert(result, parent) 168 | parent = parent:try_get_parent() 169 | while parent do 170 | table.insert(result, parent) 171 | parent = parent:try_get_parent() 172 | end 173 | return result 174 | end 175 | 176 | function Path:get_file_name() 177 | if self._value == "/" then 178 | return "" 179 | end 180 | 181 | local path = _remove_trailing_seperator_if_exists(self._value) 182 | 183 | if not path:match('[\\/]') then 184 | return path 185 | end 186 | 187 | return path:match('[\\/]([^\\/]*)$') 188 | end 189 | 190 | function Path:get_extension() 191 | local result = self._value:match('%.([^%.]*)$') 192 | return result 193 | end 194 | 195 | function Path:get_file_name_without_extension() 196 | local fileName = self:get_file_name() 197 | local extension = self:get_extension() 198 | 199 | if extension == nil then 200 | return fileName 201 | end 202 | 203 | return fileName:sub(0, #fileName - #extension - 1) 204 | end 205 | 206 | function Path:is_directory() 207 | local stats = uv.fs_stat(self._value) 208 | return stats ~= nil and stats.type == "directory" 209 | end 210 | 211 | function Path:is_file() 212 | local stats = uv.fs_stat(self._value) 213 | return stats ~= nil and stats.type == "file" 214 | end 215 | 216 | function Path:delete_empty_directory() 217 | asserts.that(self:is_directory()) 218 | 219 | local success, error_message = pcall(uv.fs_rmdir, self._value) 220 | if not success then 221 | error(string.format("Failed to remove directory at '%s'. Details: %s", self._value, error_message)) 222 | end 223 | end 224 | 225 | function Path:get_sub_paths() 226 | asserts.that(self:is_directory(), "Attempted to get sub paths for non directory path '{}'", self._value) 227 | 228 | local req, err = uv.fs_scandir(self._value) 229 | if req == nil then 230 | error(string.format("Failed to open dir '%s' for scanning. Details: '%s'", self._value, err)) 231 | end 232 | 233 | local function iter() 234 | local r1, r2 = uv.fs_scandir_next(req) 235 | 236 | 237 | if not (r1 ~= nil or (r1 == nil and r2 == nil)) then 238 | error(string.format("Failure while scanning directory '%s': %s", self._value, r2)) 239 | end 240 | return r1, r2 241 | end 242 | 243 | local result = {} 244 | 245 | for name, _ in iter do 246 | table.insert(result, self:join(name)) 247 | end 248 | 249 | return result 250 | end 251 | 252 | function Path:get_sub_directories() 253 | local result = {} 254 | 255 | for _, sub_path in ipairs(self:get_sub_paths()) do 256 | if sub_path:is_directory() then 257 | table.insert(result, sub_path) 258 | end 259 | end 260 | 261 | return result 262 | end 263 | 264 | function Path:get_sub_files() 265 | local result = {} 266 | 267 | for _, sub_path in ipairs(self:get_sub_paths()) do 268 | if sub_path:is_file() then 269 | table.insert(result, sub_path) 270 | end 271 | end 272 | 273 | return result 274 | end 275 | 276 | function Path:exists() 277 | local stats = uv.fs_stat(self._value) 278 | return stats ~= nil 279 | end 280 | 281 | function Path:delete_file() 282 | asserts.that(self:is_file(), "Called delete_file for non-file at path '{}'", self._value) 283 | assert(uv.fs_unlink(self._value)) 284 | tracing.trace(_module_name, "Deleted file at path '{}'", { self._value }) 285 | end 286 | 287 | function Path:create_directory(args) 288 | if args and args.exist_ok and self:exists() then 289 | asserts.that(self:is_directory()) 290 | return 291 | end 292 | 293 | if args and args.parents then 294 | local parent = self:try_get_parent() 295 | 296 | if not parent:exists() then 297 | parent:create_directory(args) 298 | end 299 | end 300 | 301 | local success, err = uv.fs_mkdir(self._value, default_dir_permissions) 302 | if not success then 303 | error(string.format("Failed to create directory '%s': %s", self._value, err)) 304 | end 305 | end 306 | 307 | class.setup(Path, "Path", { 308 | getters = { 309 | value = "get_value", 310 | }, 311 | }) 312 | 313 | function Path.cwd() 314 | local cwd, err = uv.cwd() 315 | if cwd == nil then 316 | error(string.format("Failed to obtain current directory: %s", err)) 317 | end 318 | return Path(cwd) 319 | end 320 | 321 | return Path 322 | -------------------------------------------------------------------------------- /gen/teal_language_server/server_state.lua: -------------------------------------------------------------------------------- 1 | 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 pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local type = type; local _module_name = "server_state" 2 | 3 | 4 | local asserts = require("teal_language_server.asserts") 5 | local lsp = require("teal_language_server.lsp") 6 | local Path = require("teal_language_server.path") 7 | local lfs = require("lfs") 8 | local TealProjectConfig = require("teal_language_server.teal_project_config") 9 | local tl = require("tl") 10 | local tracing = require("teal_language_server.tracing") 11 | local class = require("teal_language_server.class") 12 | 13 | local ServerState = {} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | function ServerState:__init() 29 | self._has_initialized = false 30 | end 31 | 32 | local capabilities = { 33 | 34 | textDocumentSync = { 35 | openClose = true, 36 | change = lsp.sync_kind.Full, 37 | save = { 38 | includeText = true, 39 | }, 40 | }, 41 | hoverProvider = true, 42 | definitionProvider = true, 43 | completionProvider = { 44 | triggerCharacters = { ".", ":" }, 45 | }, 46 | signatureHelpProvider = { 47 | triggerCharacters = { "(" }, 48 | }, 49 | } 50 | 51 | function ServerState:_validate_config(c) 52 | asserts.that(type(c) == "table", "Expected table, got {}", type(c)) 53 | 54 | local function sort_in_place(t, fn) 55 | table.sort(t, fn) 56 | return t 57 | end 58 | 59 | local function from(fn, ...) 60 | local t = {} 61 | for val in fn, ... do 62 | table.insert(t, val) 63 | end 64 | return t 65 | end 66 | 67 | local function keys(t) 68 | local k 69 | return function() 70 | k = next(t, k) 71 | return k 72 | end 73 | end 74 | 75 | local function values(t) 76 | local k, v 77 | return function() 78 | k, v = next(t, k) 79 | return v 80 | end 81 | end 82 | 83 | local function get_types_in_array(val, typefn) 84 | typefn = typefn or type 85 | local set = {} 86 | for _, v in ipairs(val) do 87 | set[typefn(v)] = true 88 | end 89 | return sort_in_place(from(keys(set))) 90 | end 91 | 92 | local function get_array_type(val, default) 93 | if type(val) ~= "table" then 94 | return type(val) 95 | end 96 | local ts = get_types_in_array(val) 97 | if #ts == 0 then 98 | ts[1] = default 99 | end 100 | return "{" .. table.concat(ts, "|") .. "}" 101 | end 102 | 103 | local function get_map_type(val, default_key, default_value) 104 | if type(val) ~= "table" then 105 | return type(val) 106 | end 107 | 108 | local key_types = get_types_in_array(from(keys(val))) 109 | if #key_types == 0 then 110 | key_types[1] = default_key 111 | end 112 | 113 | 114 | local val_types = get_types_in_array(from(values(val)), get_array_type) 115 | if #val_types == 0 then 116 | val_types[1] = default_value 117 | end 118 | return "{" .. table.concat(key_types, "|") .. ":" .. table.concat(val_types, "|") .. "}" 119 | end 120 | 121 | local valid_keys = { 122 | build_dir = "string", 123 | source_dir = "string", 124 | module_name = "string", 125 | 126 | include = "{string}", 127 | exclude = "{string}", 128 | 129 | include_dir = "{string}", 130 | global_env_def = "string", 131 | scripts = "{string:{string}}", 132 | 133 | gen_compat = { ["off"] = true, ["optional"] = true, ["required"] = true }, 134 | gen_target = { ["5.1"] = true, ["5.3"] = true }, 135 | 136 | disable_warnings = "{string}", 137 | warning_error = "{string}", 138 | } 139 | 140 | local errs = {} 141 | local warnings = {} 142 | 143 | for k, v in pairs(c) do 144 | if k == "externals" then 145 | if type(v) ~= "table" then 146 | table.insert(errs, "Expected externals to be a table, got " .. type(v)) 147 | end 148 | else 149 | local valid = valid_keys[k] 150 | if not valid then 151 | table.insert(warnings, string.format("Unknown key '%s'", k)) 152 | elseif type(valid) == "table" then 153 | if not valid[v] then 154 | local sorted_keys = sort_in_place(from(keys(valid))) 155 | table.insert(errs, "Invalid value for " .. k .. ", expected one of: " .. table.concat(sorted_keys, ", ")) 156 | end 157 | else 158 | local vtype = valid:find(":") and 159 | get_map_type(v, valid:match("^{(.*):(.*)}$")) or 160 | get_array_type(v, valid:match("^{(.*)}$")) 161 | 162 | if vtype ~= valid then 163 | table.insert(errs, string.format("Expected %s to be a %s, got %s", k, valid, vtype)) 164 | end 165 | end 166 | end 167 | end 168 | 169 | local function verify_non_absolute_path(key) 170 | local val = (c)[key] 171 | if type(val) ~= "string" then 172 | 173 | return 174 | end 175 | local as_path = Path(val) 176 | if as_path:is_absolute() then 177 | table.insert(errs, string.format("Expected a non-absolute path for %s, got %s", key, as_path.value)) 178 | end 179 | end 180 | verify_non_absolute_path("source_dir") 181 | verify_non_absolute_path("build_dir") 182 | 183 | local function verify_warnings(key) 184 | local arr = (c)[key] 185 | if arr then 186 | for _, warning in ipairs(arr) do 187 | if not tl.warning_kinds[warning] then 188 | table.insert(errs, string.format("Unknown warning in %s: %q", key, warning)) 189 | end 190 | end 191 | end 192 | end 193 | verify_warnings("disable_warnings") 194 | verify_warnings("warning_error") 195 | 196 | asserts.that(#errs == 0, "Found {} errors and {} warnings in config:\n{}\n{}", #errs, #warnings, errs, warnings) 197 | 198 | if #warnings > 0 then 199 | tracing.warning(_module_name, "Found {} warnings in config:\n{}", { #warnings, warnings }) 200 | end 201 | end 202 | 203 | function ServerState:_load_config(root_dir) 204 | local config_path = root_dir:join("tlconfig.lua") 205 | if config_path:exists() == false then 206 | return {} 207 | end 208 | 209 | local success, result = pcall(dofile, config_path.value) 210 | 211 | if success then 212 | local config = result 213 | self:_validate_config(config) 214 | return config 215 | end 216 | 217 | asserts.fail("Failed to parse tlconfig: {}", result) 218 | end 219 | 220 | function ServerState:set_env(env) 221 | asserts.is_not_nil(env) 222 | self._env = env 223 | end 224 | 225 | function ServerState:get_env() 226 | asserts.is_not_nil(self._env) 227 | return self._env 228 | end 229 | 230 | function ServerState:initialize(root_dir) 231 | asserts.that(not self._has_initialized) 232 | self._has_initialized = true 233 | 234 | self._teal_project_root_dir = root_dir 235 | asserts.that(lfs.chdir(root_dir.value), "unable to chdir into {}", root_dir.value) 236 | 237 | self._config = self:_load_config(root_dir) 238 | end 239 | 240 | class.setup(ServerState, "ServerState", { 241 | getters = { 242 | capabilities = function() 243 | return capabilities 244 | end, 245 | name = function() 246 | return "teal-language-server" 247 | end, 248 | version = function() 249 | return "0.0.1" 250 | end, 251 | teal_project_root_dir = function(self) 252 | return self._teal_project_root_dir 253 | end, 254 | config = function(self) 255 | return self._config 256 | end, 257 | }, 258 | nilable_members = { 259 | '_teal_project_root_dir', '_config', '_env', 260 | }, 261 | }) 262 | 263 | return ServerState 264 | -------------------------------------------------------------------------------- /gen/teal_language_server/stdin_reader.lua: -------------------------------------------------------------------------------- 1 | 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 string = _tl_compat and _tl_compat.string or string; local _module_name = "stdin_reader" 2 | 3 | 4 | local lusc = require("lusc") 5 | local asserts = require("teal_language_server.asserts") 6 | local uv = require("luv") 7 | local tracing = require("teal_language_server.tracing") 8 | local class = require("teal_language_server.class") 9 | 10 | local StdinReader = {} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | function StdinReader:__init() 20 | self._buffer = "" 21 | self._disposed = false 22 | self._chunk_added_event = lusc.new_pulse_event() 23 | end 24 | 25 | function StdinReader:initialize() 26 | self._stdin = uv.new_pipe(false) 27 | asserts.that(self._stdin ~= nil) 28 | assert(self._stdin:open(0)) 29 | tracing.trace(_module_name, "Opened pipe for stdin. Now waiting to receive data...") 30 | 31 | assert(self._stdin:read_start(function(err, chunk) 32 | if self._disposed then 33 | return 34 | end 35 | assert(not err, err) 36 | if chunk then 37 | tracing.trace(_module_name, "Received new data chunk from stdin: {}", { chunk }) 38 | 39 | self._buffer = self._buffer .. chunk 40 | self._chunk_added_event:set() 41 | end 42 | end)) 43 | end 44 | 45 | function StdinReader:dispose() 46 | asserts.that(not self._disposed) 47 | self._disposed = true 48 | assert(self._stdin:read_stop()) 49 | self._stdin:close() 50 | tracing.debug(_module_name, "Closed pipe for stdin") 51 | end 52 | 53 | function StdinReader:read_line() 54 | asserts.that(not self._disposed) 55 | tracing.trace(_module_name, "Attempting to read line from stdin...") 56 | asserts.that(lusc.is_available()) 57 | 58 | while true do 59 | local i = self._buffer:find("\n") 60 | 61 | if i then 62 | local line = self._buffer:sub(1, i - 1) 63 | self._buffer = self._buffer:sub(i + 1) 64 | line = line:gsub("\r$", "") 65 | tracing.trace(_module_name, "Successfully parsed line from buffer: {}. Buffer is now: {}", { line, self._buffer }) 66 | return line 67 | else 68 | tracing.trace(_module_name, "No line available yet. Waiting for more data...", {}) 69 | self._chunk_added_event:await() 70 | tracing.trace(_module_name, "Checking stdin again for new line...", {}) 71 | end 72 | end 73 | end 74 | 75 | function StdinReader:read(len) 76 | asserts.that(not self._disposed) 77 | tracing.trace(_module_name, "Attempting to read {} characters from stdin...", { len }) 78 | 79 | asserts.that(lusc.is_available()) 80 | 81 | while true do 82 | if #self._buffer >= len then 83 | local data = self._buffer:sub(1, len) 84 | self._buffer = self._buffer:sub(#data + 1) 85 | return data 86 | end 87 | 88 | self._chunk_added_event:await() 89 | end 90 | end 91 | 92 | class.setup(StdinReader, "StdinReader", { 93 | nilable_members = { '_stdin' }, 94 | }) 95 | 96 | return StdinReader 97 | -------------------------------------------------------------------------------- /gen/teal_language_server/teal_project_config.lua: -------------------------------------------------------------------------------- 1 | 2 | local tl = require("tl") 3 | 4 | local TealProjectConfig = {} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | return TealProjectConfig 25 | -------------------------------------------------------------------------------- /gen/teal_language_server/trace_entry.lua: -------------------------------------------------------------------------------- 1 | 2 | local TraceEntry = { Source = {} } 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | return TraceEntry 25 | -------------------------------------------------------------------------------- /gen/teal_language_server/trace_stream.lua: -------------------------------------------------------------------------------- 1 | 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 io = _tl_compat and _tl_compat.io or io; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local os = _tl_compat and _tl_compat.os or os; local string = _tl_compat and _tl_compat.string or string 2 | local util = require("teal_language_server.util") 3 | local asserts = require("teal_language_server.asserts") 4 | local Path = require("teal_language_server.path") 5 | local TraceEntry = require("teal_language_server.trace_entry") 6 | local json = require("cjson") 7 | local uv = require("luv") 8 | local class = require("teal_language_server.class") 9 | 10 | local TraceStream = {} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | function TraceStream:__init() 22 | self._has_initialized = false 23 | self._is_initializing = false 24 | self._has_disposed = false 25 | end 26 | 27 | local function _open_write_file(path) 28 | local file = io.open(path, "w+") 29 | asserts.is_not_nil(file, "Could not open file '{}'", path) 30 | file:setvbuf("line") 31 | return file 32 | end 33 | 34 | local function _open_write_file_append(path) 35 | local file = io.open(path, "a") 36 | asserts.is_not_nil(file, "Could not open file '{}'", path) 37 | file:setvbuf("line") 38 | return file 39 | end 40 | 41 | function TraceStream:_cleanup_old_logs(dir) 42 | if not dir:is_directory() then 43 | dir:create_directory() 44 | end 45 | 46 | local current_time_sec = os.time() 47 | local max_age_sec = 60 * 60 * 24 48 | 49 | for _, file_path in ipairs(dir:get_sub_files()) do 50 | local stats = assert(uv.fs_stat(file_path.value)) 51 | local mod_time_sec = stats.mtime.sec 52 | 53 | if current_time_sec - mod_time_sec > max_age_sec then 54 | util.try({ 55 | action = function() 56 | file_path:delete_file() 57 | end, 58 | catch = function() 59 | 60 | end, 61 | }) 62 | end 63 | end 64 | end 65 | 66 | function TraceStream:_get_log_dir() 67 | local homedir = Path(assert(uv.os_homedir())) 68 | asserts.that(homedir:exists()) 69 | local log_dir = homedir:join(".cache"):join("teal-language-server") 70 | 71 | if not log_dir:is_directory() then 72 | log_dir:create_directory() 73 | end 74 | 75 | return log_dir 76 | end 77 | 78 | function TraceStream:_choose_log_file_path() 79 | local log_dir = self:_get_log_dir() 80 | self:_cleanup_old_logs(log_dir) 81 | 82 | local date = os.date("*t") 83 | local pid = uv.os_getpid() 84 | 85 | return log_dir:join(string.format("%d-%d-%d_%d.txt", date.year, date.month, date.day, pid)) 86 | end 87 | 88 | function TraceStream:initialize() 89 | asserts.that(not self._is_initializing) 90 | self._is_initializing = true 91 | 92 | asserts.that(not self._has_initialized) 93 | self._has_initialized = true 94 | 95 | asserts.is_nil(self._file_stream) 96 | 97 | self._file_stream = _open_write_file(self.log_path.value) 98 | self._is_initializing = false 99 | end 100 | 101 | function TraceStream:_close_file() 102 | asserts.is_not_nil(self._file_stream) 103 | self._file_stream:close() 104 | end 105 | 106 | function TraceStream:rename_output_file(new_name) 107 | if self._file_stream ~= nil then 108 | self:_close_file() 109 | end 110 | 111 | local new_path = self:_get_log_dir():join(new_name .. ".log") 112 | uv.fs_rename(self._log_path.value, new_path.value) 113 | self._log_path = new_path 114 | self._file_stream = _open_write_file_append(self._log_path.value) 115 | end 116 | 117 | function TraceStream:flush() 118 | if self._has_disposed or self._is_initializing then 119 | return 120 | end 121 | 122 | if self._file_stream ~= nil then 123 | asserts.is_not_nil(self._file_stream) 124 | self._file_stream:flush() 125 | end 126 | end 127 | 128 | function TraceStream:dispose() 129 | asserts.that(not self._has_disposed) 130 | asserts.that(not self._is_initializing) 131 | 132 | self._has_disposed = true 133 | 134 | if not self._has_initialized then 135 | return 136 | end 137 | 138 | if self._file_stream ~= nil then 139 | self:_close_file() 140 | end 141 | end 142 | 143 | function TraceStream:log_entry(entry) 144 | if self._has_disposed or self._is_initializing then 145 | return 146 | end 147 | 148 | if not self._has_initialized then 149 | self:initialize() 150 | end 151 | 152 | asserts.is_not_nil(self._file_stream) 153 | 154 | self._file_stream:write(json.encode(entry) .. "\n") 155 | self._file_stream:flush() 156 | end 157 | 158 | class.setup(TraceStream, "TraceStream", { 159 | nilable_members = { "_file_stream", "_log_path" }, 160 | getters = { 161 | log_path = function(self) 162 | if self._log_path == nil then 163 | self._log_path = self:_choose_log_file_path() 164 | asserts.is_not_nil(self._log_path) 165 | end 166 | return self._log_path 167 | end, 168 | }, 169 | }) 170 | 171 | return TraceStream 172 | -------------------------------------------------------------------------------- /gen/teal_language_server/tracing.lua: -------------------------------------------------------------------------------- 1 | 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 os = _tl_compat and _tl_compat.os or os; local table = _tl_compat and _tl_compat.table or table 2 | local TraceEntry = require("teal_language_server.trace_entry") 3 | local asserts = require("teal_language_server.asserts") 4 | local tracing_util = require("teal_language_server.tracing_util") 5 | local uv = require("luv") 6 | 7 | local tracing = {} 8 | 9 | 10 | local _streams = {} 11 | 12 | local _level_trace = 0 13 | local _level_debug = 1 14 | local _level_info = 2 15 | local _level_warning = 3 16 | local _level_error = 4 17 | 18 | local level_order_from_str = { 19 | ["TRACE"] = _level_trace, 20 | ["DEBUG"] = _level_debug, 21 | ["INFO"] = _level_info, 22 | ["WARNING"] = _level_warning, 23 | ["ERROR"] = _level_error, 24 | } 25 | 26 | local _min_level = "TRACE" 27 | local _min_level_number = level_order_from_str[_min_level] 28 | 29 | local function get_unix_timestamp() 30 | return os.time() 31 | end 32 | 33 | local _load_start_time = nil 34 | 35 | local function get_ref_time_seconds() 36 | return uv.hrtime() / 1e9 37 | end 38 | 39 | local function get_relative_time() 40 | if _load_start_time == nil then 41 | _load_start_time = get_ref_time_seconds() 42 | asserts.is_not_nil(_load_start_time) 43 | end 44 | 45 | return get_ref_time_seconds() - _load_start_time 46 | end 47 | 48 | function tracing._is_level_enabled(_log_module, level) 49 | if _min_level_number > level then 50 | return false 51 | end 52 | 53 | if #_streams == 0 then 54 | return false 55 | end 56 | 57 | return true 58 | end 59 | 60 | function tracing.add_stream(stream) 61 | asserts.is_not_nil(stream) 62 | table.insert(_streams, stream) 63 | end 64 | 65 | function tracing.get_min_level() 66 | return _min_level 67 | end 68 | 69 | function tracing.set_min_level(level) 70 | _min_level = level 71 | _min_level_number = level_order_from_str[level] 72 | end 73 | 74 | local function create_entry(module, level, message_template, message_args) 75 | if message_args == nil then 76 | message_args = {} 77 | end 78 | 79 | local formatted_message = tracing_util.custom_format(message_template, message_args) 80 | 81 | return { 82 | timestamp = get_unix_timestamp(), 83 | time = get_relative_time(), 84 | level = level, 85 | module = module, 86 | message = formatted_message, 87 | } 88 | end 89 | 90 | function tracing.log(module, level, message, fields) 91 | asserts.is_not_nil(message, "Must provide a non nil value for message") 92 | asserts.that(fields == nil or type(fields) == "table", "Invalid value for fields") 93 | 94 | local entry = create_entry(module, level, message, fields) 95 | 96 | asserts.is_not_nil(entry.message) 97 | 98 | for _, stream in ipairs(_streams) do 99 | stream(entry) 100 | end 101 | end 102 | 103 | function tracing.trace(module, message, fields) 104 | if tracing._is_level_enabled(module, _level_trace) then 105 | tracing.log(module, "TRACE", message, fields) 106 | end 107 | end 108 | 109 | function tracing.debug(module, message, fields) 110 | if tracing._is_level_enabled(module, _level_debug) then 111 | tracing.log(module, "DEBUG", message, fields) 112 | end 113 | end 114 | 115 | function tracing.info(module, message, fields) 116 | if tracing._is_level_enabled(module, _level_info) then 117 | tracing.log(module, "INFO", message, fields) 118 | end 119 | end 120 | 121 | function tracing.warning(module, message, fields) 122 | if tracing._is_level_enabled(module, _level_warning) then 123 | tracing.log(module, "WARNING", message, fields) 124 | end 125 | end 126 | 127 | function tracing.error(module, message, fields) 128 | if tracing._is_level_enabled(module, _level_error) then 129 | tracing.log(module, "ERROR", message, fields) 130 | end 131 | end 132 | 133 | return tracing 134 | -------------------------------------------------------------------------------- /gen/teal_language_server/tracing_util.lua: -------------------------------------------------------------------------------- 1 | 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 string = _tl_compat and _tl_compat.string or string 2 | local inspect = require("inspect") 3 | 4 | local tracing_util = {} 5 | 6 | 7 | 8 | 9 | 10 | 11 | function tracing_util.custom_tostring(value, formatting) 12 | local value_type = type(value) 13 | 14 | if formatting == nil or formatting == "" or formatting == "l" then 15 | if value_type == "thread" then 16 | return "" 17 | elseif value_type == "function" then 18 | return "" 19 | elseif value_type == "string" then 20 | if formatting == "l" then 21 | return value 22 | else 23 | return "'" .. (value) .. "'" 24 | end 25 | else 26 | return tostring(value) 27 | end 28 | end 29 | 30 | if formatting == "@" then 31 | return inspect(value, { indent = "", newline = " " }) 32 | end 33 | 34 | 35 | return string.format(formatting, value) 36 | end 37 | 38 | function tracing_util.custom_format(message, message_args) 39 | local count = 0 40 | 41 | 42 | local pattern = "{(.-)}" 43 | 44 | local expanded_message = string.gsub(message, pattern, function(formatting) 45 | count = count + 1 46 | 47 | local field_value = message_args[count] 48 | return tracing_util.custom_tostring(field_value, formatting) 49 | end) 50 | 51 | return expanded_message 52 | end 53 | 54 | return tracing_util 55 | -------------------------------------------------------------------------------- /gen/teal_language_server/uri.lua: -------------------------------------------------------------------------------- 1 | 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 pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string 2 | local asserts = require("teal_language_server.asserts") 3 | local util = require("teal_language_server.util") 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | local Uri = {} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | local function find_patt(str, patt, pos) 28 | pos = pos or 1 29 | local s, e = str:find(patt, pos) 30 | if s then 31 | return str:sub(s, e), s, e 32 | end 33 | end 34 | 35 | local function get_next_key_and_patt(current) 36 | if current == "://" then 37 | return "authority", "/" 38 | elseif current == "/" then 39 | return "path", "[?#]" 40 | elseif current == "?" then 41 | return "query", "#" 42 | elseif current == "#" then 43 | return "fragment", "$" 44 | end 45 | end 46 | 47 | function Uri.parse(text) 48 | if not text then 49 | return nil 50 | end 51 | 52 | 53 | 54 | local parsed = {} 55 | local last = 1 56 | local next_key = "scheme" 57 | local next_patt = "://" 58 | 59 | while next_patt do 60 | local char, s, e = find_patt(text, next_patt, last) 61 | parsed[next_key] = text:sub(last, (s or 0) - 1) 62 | 63 | next_key, next_patt = get_next_key_and_patt(char) 64 | last = (e or last) + 65 | (next_key == "path" and 0 or 1) 66 | end 67 | 68 | for k, v in pairs(parsed) do 69 | if #v == 0 then 70 | parsed[k] = nil 71 | end 72 | end 73 | 74 | 75 | if parsed.authority and not parsed.path then 76 | parsed.path = "" 77 | end 78 | 79 | 80 | 81 | if util.get_platform() == "windows" and util.string_starts_with(parsed.path, "/") then 82 | parsed.path = parsed.path:sub(2) 83 | end 84 | 85 | 86 | if not (parsed.scheme and parsed.path) then 87 | return nil 88 | end 89 | 90 | 91 | if not parsed.authority and parsed.path:sub(1, 2) == "//" then 92 | return nil 93 | end 94 | 95 | return parsed 96 | end 97 | 98 | function Uri.path_from_uri(s) 99 | local parsed = Uri.parse(s) 100 | asserts.that(parsed.scheme == "file", "uri " .. tostring(s) .. " is not a file") 101 | return parsed.path 102 | end 103 | 104 | function Uri.uri_from_path(path) 105 | return { 106 | scheme = "file", 107 | authority = "", 108 | path = path, 109 | query = nil, 110 | fragment = nil, 111 | } 112 | end 113 | 114 | function Uri.tostring(u) 115 | return u.scheme .. "://" .. 116 | (u.authority or "") .. 117 | (u.path or "") .. 118 | (u.query and "?" .. u.query or "") .. 119 | (u.fragment and "#" .. u.fragment or "") 120 | end 121 | 122 | return Uri 123 | -------------------------------------------------------------------------------- /gen/teal_language_server/util.lua: -------------------------------------------------------------------------------- 1 | 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 debug = _tl_compat and _tl_compat.debug or debug; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local type = type; local xpcall = _tl_compat and _tl_compat.xpcall or xpcall; local _module_name = "util" 2 | 3 | local uv = require("luv") 4 | local tracing = require("teal_language_server.tracing") 5 | 6 | local util = { TryOpts = {} } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | local _uname_info = nil 22 | local _os_type = nil 23 | 24 | local function _get_uname_info() 25 | if _uname_info == nil then 26 | _uname_info = uv.os_uname() 27 | assert(_uname_info ~= nil) 28 | end 29 | 30 | return _uname_info 31 | end 32 | 33 | local function _on_error(error_obj) 34 | return debug.traceback(error_obj, 2) 35 | end 36 | 37 | function util.string_escape_special_chars(value) 38 | 39 | 40 | value = value:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0") 41 | 42 | 43 | return value 44 | end 45 | 46 | function util.string_starts_with(str, prefix) 47 | return str:sub(1, #prefix) == prefix 48 | end 49 | 50 | function util.string_split(str, delimiter) 51 | 52 | 53 | 54 | assert(#str > 0, "Unclear how to split an empty string") 55 | 56 | assert(delimiter ~= nil, "missing delimiter") 57 | assert(type(delimiter) == "string") 58 | assert(#delimiter > 0) 59 | 60 | local num_delimiter_chars = #delimiter 61 | 62 | delimiter = util.string_escape_special_chars(delimiter) 63 | 64 | local start_index = 1 65 | local result = {} 66 | 67 | while true do 68 | local delimiter_index, _ = str:find(delimiter, start_index) 69 | 70 | if delimiter_index == nil then 71 | table.insert(result, str:sub(start_index)) 72 | break 73 | end 74 | 75 | table.insert(result, str:sub(start_index, delimiter_index - 1)) 76 | 77 | start_index = delimiter_index + num_delimiter_chars 78 | end 79 | 80 | return result 81 | end 82 | 83 | function util.string_join(delimiter, items) 84 | assert(type(delimiter) == "string") 85 | assert(items ~= nil) 86 | 87 | local result = '' 88 | for _, item in ipairs(items) do 89 | if #result ~= 0 then 90 | result = result .. delimiter 91 | end 92 | result = result .. tostring(item) 93 | end 94 | return result 95 | end 96 | 97 | function util.get_platform() 98 | if _os_type == nil then 99 | local raw_os_name = string.lower(_get_uname_info().sysname) 100 | 101 | if raw_os_name == "linux" then 102 | _os_type = "linux" 103 | elseif raw_os_name:find("darwin") ~= nil then 104 | _os_type = "osx" 105 | elseif raw_os_name:find("windows") ~= nil or raw_os_name:find("mingw") ~= nil then 106 | _os_type = "windows" 107 | else 108 | tracing.warning(_module_name, "Unrecognized platform {}", { raw_os_name }) 109 | _os_type = "unknown" 110 | end 111 | end 112 | 113 | return _os_type 114 | end 115 | 116 | function util.try(t) 117 | local success, ret_value = xpcall(t.action, _on_error) 118 | if success then 119 | if t.finally then 120 | t.finally() 121 | end 122 | return ret_value 123 | end 124 | if not t.catch then 125 | if t.finally then 126 | t.finally() 127 | end 128 | error(ret_value, 2) 129 | end 130 | success, ret_value = xpcall((function() 131 | return t.catch(ret_value) 132 | end), _on_error) 133 | if t.finally then 134 | t.finally() 135 | end 136 | if success then 137 | return ret_value 138 | end 139 | return error(ret_value, 2) 140 | end 141 | 142 | return util 143 | -------------------------------------------------------------------------------- /luarocks.lock: -------------------------------------------------------------------------------- 1 | return { 2 | build_dependencies = { 3 | luafilesystem = "1.8.0-1", 4 | ["luarocks-build-tree-sitter-cli"] = "0.0.2-1", 5 | ["luarocks-build-treesitter-parser"] = "6.0.0-1" 6 | }, 7 | dependencies = { 8 | argparse = "0.7.1-1", 9 | compat53 = "0.14.4-1", 10 | inspect = "3.1.3-0", 11 | ["ltreesitter-ts"] = "0.0.1-1", 12 | ["lua-cjson"] = "2.1.0.10-1", 13 | luafilesystem = "1.8.0-1", 14 | lusc_luv = "4.0.1-1", 15 | luv = "1.50.0-1", 16 | tl = "0.24.4-1", 17 | ["tree-sitter-cli"] = "0.24.4-2", 18 | ["tree-sitter-teal"] = "0.0.33-1" 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /scripts/create_binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # yeah, this is a bit of a hack by utilizing hererocks and reconfigring it to be portable but it works! 4 | 5 | set -e 6 | 7 | python3 -m venv .venv 8 | source .venv/bin/activate 9 | python3 -m pip install hererocks 10 | 11 | hererocks -l "@v5.4.7" -r "@v3.11.1" tls 12 | 13 | source tls/bin/activate 14 | luarocks make 15 | 16 | # TODO: see if we can just make this a build dependency? 17 | luarocks remove --force tree-sitter-cli 18 | 19 | rm -f ./tls/bin/activate* ./tls/bin/get_deactivated_path.lua 20 | rm -f ./tls/bin/json2lua ./tls/bin/lua2json 21 | rm -f ./tls/bin/luarocks ./tls/bin/luarocks-admin ./tls/bin/tl 22 | rm -rf ./tls/include 23 | 24 | tls_dir="$(pwd)/tls/" 25 | sed -i '' -e '2i\ 26 | cd "$(dirname "$0")"' ./tls/bin/teal-language-server 27 | sed -i '' -e "s*$tls_dir*../*g" ./tls/bin/teal-language-server 28 | 29 | teal-language-server --help 30 | -------------------------------------------------------------------------------- /scripts/create_windows_binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # make this our "main" folder 6 | mv luarocks teal-language-server 7 | 8 | # install lua 9 | mv .lua/bin/lua.exe teal-language-server/bin/ 10 | mv .lua/bin/lua54.dll teal-language-server/bin/ 11 | 12 | # clean out bin 13 | cd teal-language-server/bin 14 | rm -f json2lua.bat lua2json.bat tl.bat 15 | 16 | # modify teal-language-server.bat 17 | # a bit hacky, but should preseve some version numbers 18 | sed -i 's*set "LUAROCKS_SYSCONFDIR=C:\\Program Files\\luarocks"*cd /D "%~dp0"*g' teal-language-server.bat 19 | sed -i 's*D:\\a\\teal-language-server\\teal-language-server\\.lua\\bin\\lua.exe*.\\lua.exe*g' teal-language-server.bat 20 | sed -i 's*D:\\\\a\\\\teal-language-server\\\\teal-language-server\\\\luarocks*..*g' teal-language-server.bat 21 | sed -i 's*D:\\a\\teal-language-server\\teal-language-server\\luarocks*..*g' teal-language-server.bat 22 | -------------------------------------------------------------------------------- /scripts/generate_lua.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | call %~dp0setup_local_luarocks.bat 4 | cd %~dp0\.. 5 | rmdir /s /q gen 6 | mkdir gen 7 | mkdir gen\teal_language_server 8 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\args_parser.tl -o %~dp0\..\gen\teal_language_server\args_parser.lua 9 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\asserts.tl -o %~dp0\..\gen\teal_language_server\asserts.lua 10 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\constants.tl -o %~dp0\..\gen\teal_language_server\constants.lua 11 | copy src\teal_language_server\class.lua gen\teal_language_server\class.lua 12 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\document.tl -o %~dp0\..\gen\teal_language_server\document.lua 13 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\document_manager.tl -o %~dp0\..\gen\teal_language_server\document_manager.lua 14 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\env_updater.tl -o %~dp0\..\gen\teal_language_server\env_updater.lua 15 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\lsp.tl -o %~dp0\..\gen\teal_language_server\lsp.lua 16 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\lsp_events_manager.tl -o %~dp0\..\gen\teal_language_server\lsp_events_manager.lua 17 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\lsp_reader_writer.tl -o %~dp0\..\gen\teal_language_server\lsp_reader_writer.lua 18 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\main.tl -o %~dp0\..\gen\teal_language_server\main.lua 19 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\misc_handlers.tl -o %~dp0\..\gen\teal_language_server\misc_handlers.lua 20 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\path.tl -o %~dp0\..\gen\teal_language_server\path.lua 21 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\server_state.tl -o %~dp0\..\gen\teal_language_server\server_state.lua 22 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\stdin_reader.tl -o %~dp0\..\gen\teal_language_server\stdin_reader.lua 23 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\teal_project_config.tl -o %~dp0\..\gen\teal_language_server\teal_project_config.lua 24 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\trace_entry.tl -o %~dp0\..\gen\teal_language_server\trace_entry.lua 25 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\trace_stream.tl -o %~dp0\..\gen\teal_language_server\trace_stream.lua 26 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\tracing.tl -o %~dp0\..\gen\teal_language_server\tracing.lua 27 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\uri.tl -o %~dp0\..\gen\teal_language_server\uri.lua 28 | call luarocks\bin\tl.bat gen %~dp0\..\src\teal_language_server\util.tl -o %~dp0\..\gen\teal_language_server\util.lua 29 | -------------------------------------------------------------------------------- /scripts/generate_lua.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | cd `dirname $BASH_SOURCE`/.. 4 | rm -rf ./gen 5 | mkdir ./gen 6 | mkdir ./gen/teal_language_server 7 | luarocks_tree/bin/tl gen src/teal_language_server/args_parser.tl -o gen/teal_language_server/args_parser.lua 8 | luarocks_tree/bin/tl gen src/teal_language_server/asserts.tl -o gen/teal_language_server/asserts.lua 9 | cp src/teal_language_server/class.lua gen/teal_language_server/class.lua 10 | luarocks_tree/bin/tl gen src/teal_language_server/constants.tl -o gen/teal_language_server/constants.lua 11 | luarocks_tree/bin/tl gen src/teal_language_server/document.tl -o gen/teal_language_server/document.lua 12 | luarocks_tree/bin/tl gen src/teal_language_server/document_manager.tl -o gen/teal_language_server/document_manager.lua 13 | luarocks_tree/bin/tl gen src/teal_language_server/env_updater.tl -o gen/teal_language_server/env_updater.lua 14 | luarocks_tree/bin/tl gen src/teal_language_server/lsp.tl -o gen/teal_language_server/lsp.lua 15 | luarocks_tree/bin/tl gen src/teal_language_server/lsp_events_manager.tl -o gen/teal_language_server/lsp_events_manager.lua 16 | luarocks_tree/bin/tl gen src/teal_language_server/lsp_formatter.tl -o gen/teal_language_server/lsp_formatter.lua 17 | luarocks_tree/bin/tl gen src/teal_language_server/lsp_reader_writer.tl -o gen/teal_language_server/lsp_reader_writer.lua 18 | luarocks_tree/bin/tl gen src/teal_language_server/main.tl -o gen/teal_language_server/main.lua 19 | luarocks_tree/bin/tl gen src/teal_language_server/misc_handlers.tl -o gen/teal_language_server/misc_handlers.lua 20 | luarocks_tree/bin/tl gen src/teal_language_server/path.tl -o gen/teal_language_server/path.lua 21 | luarocks_tree/bin/tl gen src/teal_language_server/server_state.tl -o gen/teal_language_server/server_state.lua 22 | luarocks_tree/bin/tl gen src/teal_language_server/stdin_reader.tl -o gen/teal_language_server/stdin_reader.lua 23 | luarocks_tree/bin/tl gen src/teal_language_server/teal_project_config.tl -o gen/teal_language_server/teal_project_config.lua 24 | luarocks_tree/bin/tl gen src/teal_language_server/trace_entry.tl -o gen/teal_language_server/trace_entry.lua 25 | luarocks_tree/bin/tl gen src/teal_language_server/trace_stream.tl -o gen/teal_language_server/trace_stream.lua 26 | luarocks_tree/bin/tl gen src/teal_language_server/tracing.tl -o gen/teal_language_server/tracing.lua 27 | luarocks_tree/bin/tl gen src/teal_language_server/tracing_util.tl -o gen/teal_language_server/tracing_util.lua 28 | luarocks_tree/bin/tl gen src/teal_language_server/uri.tl -o gen/teal_language_server/uri.lua 29 | luarocks_tree/bin/tl gen src/teal_language_server/util.tl -o gen/teal_language_server/util.lua 30 | -------------------------------------------------------------------------------- /scripts/lint_teal.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | REM Require this is called manually make output more clear 4 | REM call %~dp0setup_local_luarocks.bat 5 | cd %~dp0\.. 6 | call luarocks\bin\tlcheck.bat src 7 | echo Linting complete 8 | endlocal 9 | 10 | -------------------------------------------------------------------------------- /scripts/lint_teal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "$(dirname "$0")/.." 4 | luarocks_tree/bin/tlcheck src 5 | echo "Linting complete." 6 | -------------------------------------------------------------------------------- /scripts/run_all.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | call %~dp0generate_lua.bat 5 | if %errorlevel% neq 0 exit /b %errorlevel% 6 | call %~dp0lint_teal.bat 7 | if %errorlevel% neq 0 exit /b %errorlevel% 8 | 9 | REM We run setup_local_luarocks again here even though it is already run in generate_lua.bat 10 | REM so that teal-language-server is deployed to luarocks tree 11 | call %~dp0setup_local_luarocks.bat 12 | if %errorlevel% neq 0 exit /b %errorlevel% 13 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Navigate to the root of the repo 5 | cd "$(dirname "$0")/.." 6 | 7 | # Set the local LuaRocks path 8 | LUAROCKS_TREE="$(pwd)/luarocks_tree" 9 | 10 | # Run unit tests 11 | echo "Run LuaRocks tests:" 12 | luarocks test --tree="$LUAROCKS_TREE" 13 | -------------------------------------------------------------------------------- /scripts/setup_local_luarocks.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | rem Navigate to the root of the repo 5 | cd %~dp0\.. 6 | 7 | rem Check if the local luarocks tree is already initialized 8 | if not exist "luarocks" ( 9 | echo Initializing local LuaRocks tree... 10 | luarocks init --tree=./luarocks 11 | ) else ( 12 | echo Local LuaRocks tree already exists. 13 | ) 14 | 15 | rem Set the local LuaRocks path 16 | set "LUAROCKS_TREE=%~dp0\..\luarocks" 17 | 18 | rem Install project dependencies from the rockspec 19 | echo Installing project dependencies... 20 | call luarocks make --tree=!LUAROCKS_TREE! 21 | 22 | echo Installing tlcheck for linting... 23 | call luarocks install tlcheck --tree=!LUAROCKS_TREE! 24 | 25 | rem Confirm installations 26 | echo Installed LuaRocks packages: 27 | call luarocks list --tree=!LUAROCKS_TREE! 28 | 29 | endlocal 30 | 31 | -------------------------------------------------------------------------------- /scripts/setup_local_luarocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Navigate to the root of the repo 5 | cd "$(dirname "$0")/.." 6 | 7 | # Set the local LuaRocks path 8 | LUAROCKS_TREE="$(pwd)/luarocks_tree" 9 | 10 | # setup local LuaRocks 11 | luarocks init --tree="$LUAROCKS_TREE" 12 | PATH="$LUAROCKS_TREE/bin":"$PATH" 13 | export PATH 14 | 15 | # Install project dependencies from the rockspec 16 | echo "Installing project dependencies..." 17 | luarocks make --tree="$LUAROCKS_TREE" 18 | 19 | echo "Installing tlcheck for linting..." 20 | luarocks install tlcheck --tree="$LUAROCKS_TREE" 21 | 22 | # Confirm installations 23 | echo "Installed LuaRocks packages:" 24 | luarocks list --tree="$LUAROCKS_TREE" 25 | -------------------------------------------------------------------------------- /spec/document_tree_sitter_spec.lua: -------------------------------------------------------------------------------- 1 | local Document = require("teal_language_server.document") 2 | local ServerState = require("teal_language_server.server_state") 3 | 4 | describe("tree_sitter_parser", function() 5 | it("should analyze basic function defintions", function() 6 | local content = [[local function a() end]] 7 | 8 | local doc = Document("test-uri", content, 1, {}, ServerState()) 9 | 10 | local node_info = doc:tree_sitter_token(0, 2) 11 | assert.same(node_info.type, "local") 12 | assert.same(node_info.parent_type, "function_statement") 13 | 14 | node_info = doc:tree_sitter_token(0, 8) 15 | assert.same(node_info.type, "function") 16 | end) 17 | 18 | it("returns nil on empty char", function() 19 | local content = [[local ]] 20 | 21 | local doc = Document("test-uri", content, 1, {}, ServerState()) 22 | 23 | local node_info = doc:tree_sitter_token(0, 6) 24 | assert.is_nil(node_info) 25 | end) 26 | 27 | it("returns on empty content", function() 28 | local content = [[]] 29 | 30 | local doc = Document("test-uri", content, 1, {}, ServerState()) 31 | 32 | local node_info = doc:tree_sitter_token(0, 0) 33 | assert.same(node_info.type, "program") 34 | 35 | -- and on chars that aren't there yet 36 | local node_info = doc:tree_sitter_token(0, 6) 37 | assert.same(node_info.type, "program") 38 | end) 39 | 40 | it("identifies function calls and vars", function() 41 | local content = [[local dir = require("pl.dir")]] 42 | 43 | local doc = Document("test-uri", content, 1, {}, ServerState()) 44 | 45 | local node_info = doc:tree_sitter_token(0, 16) 46 | assert.same(node_info.parent_type, "function_call") 47 | 48 | local node_info = doc:tree_sitter_token(0, 8) 49 | assert.same(node_info.parent_type, "var") 50 | 51 | local node_info = doc:tree_sitter_token(0, 3) 52 | assert.same(node_info.parent_type, "var_declaration") 53 | end) 54 | 55 | it("should recognize when at a .", function() 56 | local content = [[ 57 | local dir = require("pl.dir") 58 | dir. 59 | ]] 60 | 61 | local doc = Document("test-uri", content, 1, {}, ServerState()) 62 | 63 | local node_info = doc:tree_sitter_token(1, 3) 64 | assert.same(node_info.type, ".") 65 | assert.same(node_info.parent_type, "ERROR") 66 | assert.same(node_info.preceded_by, "dir") 67 | end) 68 | 69 | it("should recognize when at a :", function() 70 | local content = [[ 71 | local t = "fruit" 72 | t: 73 | ]] 74 | 75 | local doc = Document("test-uri", content, 1, {}, ServerState()) 76 | 77 | local node_info = doc:tree_sitter_token(1, 1) 78 | assert.same(node_info.type, ":") 79 | assert.same(node_info.parent_type, "ERROR") 80 | assert.same(node_info.preceded_by, "t") 81 | end) 82 | 83 | it("should recognize a nested .", function() 84 | local content = [[string.byte(t.,]] 85 | 86 | local doc = Document("test-uri", content, 1, {}, ServerState()) 87 | 88 | local node_info = doc:tree_sitter_token(0, 13) 89 | assert.same(node_info.type, ".") 90 | assert.same(node_info.parent_type, "ERROR") 91 | assert.same(node_info.preceded_by, "t") 92 | 93 | local node_info = doc:tree_sitter_token(0, 6) 94 | assert.same(node_info.type, ".") 95 | assert.same(node_info.parent_type, "index") 96 | assert.same(node_info.preceded_by, "string") 97 | end) 98 | 99 | it("should handle chained .", function() 100 | local content = [[lsp.completion_context.]] 101 | 102 | local doc = Document("test-uri", content, 1, {}, ServerState()) 103 | 104 | local node_info = doc:tree_sitter_token(0, 22) 105 | assert.same(node_info.type, ".") 106 | assert.same(node_info.parent_type, "ERROR") 107 | assert.same(node_info.preceded_by, "lsp.completion_context") 108 | 109 | local node_info = doc:tree_sitter_token(0, 19) 110 | assert.same(node_info.type, "identifier") 111 | assert.same(node_info.parent_type, "index") 112 | assert.same(node_info.parent_source, "lsp.completion_context") 113 | assert.is_nil(node_info.preceded_by) 114 | 115 | local node_info = doc:tree_sitter_token(0, 3) 116 | assert.same(node_info.type, ".") 117 | assert.same(node_info.parent_type, "index") 118 | assert.same(node_info.preceded_by, "lsp") 119 | end) 120 | 121 | it("should handle a variable defintion", function() 122 | local content = [[local fruit: string = "thing"]] 123 | 124 | local doc = Document("test-uri", content, 1, {}, ServerState()) 125 | 126 | local node_info = doc:tree_sitter_token(0, 9) 127 | assert.same(node_info.parent_type, "var") 128 | assert.same(node_info.source, "fruit") 129 | 130 | local node_info = doc:tree_sitter_token(0, 16) 131 | assert.same(node_info.parent_type, "simple_type") 132 | assert.same(node_info.source, "string") 133 | 134 | local node_info = doc:tree_sitter_token(0, 26) 135 | assert.same(node_info.parent_type, "string") 136 | assert.same(node_info.source, "thing") 137 | end) 138 | 139 | it("should handle a basic self function", function() 140 | local content = [[ 141 | function Point:move(dx: number, dy: number) 142 | self.x = self.x + dx 143 | self.y = self.y + dy 144 | end 145 | ]] 146 | 147 | local doc = Document("test-uri", content, 1, {}, ServerState()) 148 | 149 | local node_info = doc:tree_sitter_token(0, 13) 150 | assert.same(node_info.parent_type, "function_name") 151 | assert.same(node_info.source, "Point") 152 | 153 | local node_info = doc:tree_sitter_token(0, 18) 154 | assert.same(node_info.parent_type, "function_name") 155 | assert.same(node_info.source, "move") 156 | 157 | local node_info = doc:tree_sitter_token(0, 33) 158 | assert.same(node_info.parent_type, "arg") 159 | assert.same(node_info.source, "dy") 160 | 161 | local node_info = doc:tree_sitter_token(2, 6) 162 | assert.same(node_info.parent_type, "index") 163 | assert.same(node_info.self_type, "Point") 164 | end) 165 | 166 | it("", function() 167 | local content = [[ 168 | function Document:thing() 169 | function fruit() 170 | self._something:fruit 171 | end 172 | end 173 | ]] 174 | 175 | local doc = Document("test-uri", content, 1, {}, ServerState()) 176 | 177 | local node_info = doc:tree_sitter_token(2, 9) 178 | assert.same(node_info.type, "identifier") 179 | assert.same(node_info.source, "_something") 180 | assert.same(node_info.parent_type, "function_name") 181 | assert.same(node_info.parent_source, "self._something:fruit") 182 | assert.same(node_info.self_type, "Document") 183 | 184 | local node_info = doc:tree_sitter_token(2, 20) 185 | assert.same(node_info.type, "identifier") 186 | assert.same(node_info.source, "fruit") 187 | assert.same(node_info.parent_type, "function_name") 188 | assert.same(node_info.parent_source, "self._something:fruit") 189 | assert.same(node_info.self_type, "Document") 190 | 191 | local node_info = doc:tree_sitter_token(2, 15) 192 | assert.same(node_info.type, ":") 193 | assert.same(node_info.source, ":") 194 | assert.same(node_info.parent_type, "function_name") 195 | assert.same(node_info.parent_source, "self._something:fruit") 196 | assert.same(node_info.preceded_by, "_something") 197 | assert.same(node_info.self_type, "Document") 198 | end) 199 | 200 | it("should handle even more nested .'s", function() 201 | local content = [[lsp.orange.depot.box]] 202 | 203 | local doc = Document("test-uri", content, 1, {}, ServerState()) 204 | 205 | local node_info = doc:tree_sitter_token(0, 6) 206 | assert.same(node_info.type, "identifier") 207 | assert.same(node_info.source, "orange") 208 | assert.same(node_info.parent_type, "index") 209 | assert.same(node_info.parent_source, "lsp.orange") 210 | 211 | local node_info = doc:tree_sitter_token(0, 13) 212 | assert.same(node_info.type, "identifier") 213 | assert.same(node_info.source, "depot") 214 | assert.same(node_info.parent_type, "index") 215 | assert.same(node_info.parent_source, "lsp.orange.depot") 216 | 217 | local node_info = doc:tree_sitter_token(0, 16) 218 | assert.same(node_info.type, ".") 219 | assert.same(node_info.source, ".") 220 | assert.same(node_info.parent_type, "index") 221 | assert.same(node_info.parent_source, "lsp.orange.depot.box") 222 | assert.same(node_info.preceded_by, "lsp.orange.depot") 223 | 224 | end) 225 | 226 | it("should handle partial method chains", function() 227 | local content = [[string.byte(t:fruit():,]] 228 | 229 | local doc = Document("test-uri", content, 1, {}, ServerState()) 230 | 231 | local node_info = doc:tree_sitter_token(0, 21) 232 | assert.same(node_info.type, ":") 233 | assert.same(node_info.source, ":") 234 | assert.same(node_info.parent_type, "ERROR") 235 | assert.same(node_info.parent_source, "string.byte(t:fruit():,") 236 | assert.same(node_info.preceded_by, "t:fruit()") 237 | end) 238 | 239 | it("should handle real code pulling out self", function() 240 | local content = [[ 241 | function MiscHandlers:initialize() 242 | self:_add_handler("initialize", self._on_initialize) 243 | self:_add_handler("initialized", self._on_initialized) 244 | self:_add_handler("textDocument/didOpen", self._on_did_open) 245 | self:_add_handler("textDocument/didClose", self._on_did_close) 246 | self:_add_handler("textDocument/didSave", self._on_did_save) 247 | self:_add_handler("textDocument/didChange", self._on_did_change) 248 | self:_add_handler("textDocument/completion", self._on_completion) 249 | self: 250 | -- self:_add_handler("textDocument/signatureHelp", self._on_signature_help) 251 | -- self:_add_handler("textDocument/definition", self._on_definition) 252 | -- self:_add_handler("textDocument/hover", self._on_hover) 253 | end 254 | ]] 255 | 256 | local doc = Document("test-uri", content, 1, {}, ServerState()) 257 | 258 | local node_info = doc:tree_sitter_token(8, 7) 259 | assert.same(node_info.type, ":") 260 | assert.same(node_info.source, ":") 261 | assert.same(node_info.parent_type, "method_index") 262 | assert.same(node_info.preceded_by, "self") 263 | assert.same(node_info.self_type, "MiscHandlers") 264 | 265 | end) 266 | 267 | 268 | it("should work with more real use cases", function() 269 | local content = [[ 270 | function MiscHandlers:_on_hover(params:lsp.Method.Params, id:integer):nil 271 | local pos = params.position as lsp.Position 272 | local node_info, doc = self:_get_node_info(params, pos) 273 | if node_info == nil then 274 | self._lsp_reader_writer:send_rpc(id, { 275 | contents = { "Unknown Token:", " Unable to determine what token is under cursor " }, 276 | range = { 277 | start = lsp.position(pos.line, pos.character), 278 | ["end"] = lsp.position(pos.line, pos.character), 279 | }, 280 | }) 281 | return 282 | end 283 | end]] 284 | 285 | local doc = Document("test-uri", content, 1, {}, ServerState()) 286 | 287 | local node_info = doc:tree_sitter_token(0, 35) 288 | assert.same(node_info.type, "identifier") 289 | assert.same(node_info.source, "params") 290 | assert.same(node_info.parent_type, "arg") 291 | assert.same(node_info.parent_source, "params:lsp.Method.Params") 292 | 293 | local node_info = doc:tree_sitter_token(1, 35) 294 | assert.same(node_info.type, "identifier") 295 | assert.same(node_info.source, "position") 296 | assert.same(node_info.parent_type, "index") 297 | assert.same(node_info.parent_source, "params.position") 298 | 299 | local node_info = doc:tree_sitter_token(2, 35) 300 | assert.same(node_info.type, "identifier") 301 | assert.same(node_info.source, "_get_node_info") 302 | assert.same(node_info.parent_type, "method_index") 303 | assert.same(node_info.parent_source, "self:_get_node_info") 304 | assert.same(node_info.self_type, "MiscHandlers") 305 | 306 | local node_info = doc:tree_sitter_token(8, 52) 307 | assert.same(node_info.type, "identifier") 308 | assert.same(node_info.source, "character") 309 | assert.same(node_info.parent_type, "index") 310 | assert.same(node_info.parent_source, "pos.character") 311 | 312 | end) 313 | 314 | it("should handle getting function signatures with valid syntax", function() 315 | local content = [[tracing.warning()]] 316 | 317 | local doc = Document("test-uri", content, 1, {}, ServerState()) 318 | 319 | local node_info = doc:tree_sitter_token(0, 15) 320 | assert.same(node_info.type, "(") 321 | assert.same(node_info.source, "(") 322 | assert.same(node_info.parent_type, "arguments") 323 | assert.same(node_info.parent_source, "()") 324 | assert.same(node_info.preceded_by, "tracing.warning") 325 | end) 326 | 327 | it("should handle getting function signatures with invalid syntax", function() 328 | local content = [[tracing.warning(]] 329 | 330 | local doc = Document("test-uri", content, 1, {}, ServerState()) 331 | 332 | local node_info = doc:tree_sitter_token(0, 15) 333 | assert.same(node_info.type, "(") 334 | assert.same(node_info.source, "(") 335 | assert.same(node_info.parent_type, "ERROR") 336 | assert.same(node_info.parent_source, "tracing.warning(") 337 | assert.same(node_info.preceded_by, "tracing.warning") 338 | end) 339 | 340 | it("", function() 341 | local content = [[ 342 | if indexable_parent_types[node_info.parent_type] then 343 | tks = split_by_symbols(node_info.parent_source, node_info.self_type) 344 | else 345 | tks = split_by_symbols(node_info.source, node_info.self_type) 346 | end]] 347 | local doc = Document("test-uri", content, 1, {}, ServerState()) 348 | 349 | local node_info = doc:tree_sitter_token(0, 16) 350 | assert.same(node_info.type, "identifier") 351 | assert.same(node_info.source, "indexable_parent_types") 352 | assert.same(node_info.parent_type, "index") 353 | assert.same(node_info.parent_source, "indexable_parent_types[node_info.parent_type]") 354 | end) 355 | end) 356 | -------------------------------------------------------------------------------- /src/teal_language_server/args_parser.tl: -------------------------------------------------------------------------------- 1 | 2 | local asserts = require("teal_language_server.asserts") 3 | 4 | local record args_parser 5 | enum LogMode 6 | "none" 7 | "by_date" 8 | "by_proj_path" 9 | end 10 | 11 | record CommandLineArgs 12 | verbose:boolean 13 | log_mode:LogMode 14 | end 15 | end 16 | 17 | function args_parser.parse_args():args_parser.CommandLineArgs 18 | local argparse = require("argparse") 19 | local parser = argparse("teal-language-server", "Teal Language Server") 20 | 21 | parser:option("-V --verbose", "") 22 | 23 | parser:option("-L --log-mode", "Specify approach to logging. By default it is none which means no logging. by_date names the file according to date. by_proj_path names file according to the teal project path") 24 | :choices({"none", "by_date", "by_proj_path"}) 25 | 26 | local raw_args = parser:parse() 27 | 28 | local verbose = raw_args["verbose"] as boolean 29 | local log_mode = raw_args["log_mode"] as args_parser.LogMode 30 | 31 | if log_mode == nil then 32 | log_mode = "none" 33 | else 34 | asserts.that(log_mode == "by_date" or log_mode == "by_proj_path") 35 | end 36 | 37 | local args:args_parser.CommandLineArgs = { 38 | verbose = verbose, 39 | log_mode = log_mode, 40 | } 41 | 42 | return args 43 | end 44 | 45 | return args_parser 46 | -------------------------------------------------------------------------------- /src/teal_language_server/asserts.tl: -------------------------------------------------------------------------------- 1 | 2 | local record asserts 3 | end 4 | 5 | local function _raise(format:string, ...:any) 6 | if format == nil then 7 | error("Assert hit!") 8 | else 9 | -- We use convention {} instead of %s to remain compatible with other types of logging 10 | if format:find("%%s") ~= nil then 11 | error("Unexpected assert string - should use {} instead of %s") 12 | end 13 | format = format:gsub("{}", "%%s") 14 | error(string.format(format, ...)) 15 | end 16 | end 17 | 18 | function asserts.fail(format:string, ...:any) 19 | _raise(format, ...) 20 | end 21 | function asserts.that(condition:boolean, format?:string, ...:any) 22 | if not condition then 23 | _raise(format, ...) 24 | end 25 | end 26 | 27 | function asserts.is_nil(value:any, format?:string, ...:any) 28 | if value ~= nil then 29 | if format == nil then 30 | _raise("Expected nil value but instead found '{}'", value) 31 | else 32 | _raise(format, ...) 33 | end 34 | end 35 | end 36 | 37 | function asserts.is_not_nil(value:any, format?:string, ...:any) 38 | if value == nil then 39 | if format == nil then 40 | _raise("Expected non-nil value") 41 | else 42 | _raise(format, ...) 43 | end 44 | end 45 | end 46 | 47 | return asserts 48 | -------------------------------------------------------------------------------- /src/teal_language_server/class.d.tl: -------------------------------------------------------------------------------- 1 | 2 | -- TODO - change to use generics 3 | local record SetupOptions 4 | getters:{string:string|function(any):any} 5 | setters:{string:string|function(any, any)} 6 | 7 | nilable_members: {string} 8 | 9 | interfaces: {any} 10 | attributes: {string: any} 11 | 12 | -- Closed = no new keys except in __init method 13 | closed:boolean 14 | -- Immutable = no changes to values except in __init method 15 | immutable:boolean 16 | end 17 | 18 | local record Class 19 | setup:function(rec:any, name:string, options?:SetupOptions) 20 | 21 | get_name:function(any):string 22 | try_get_name:function(any):string 23 | 24 | get_class_name_for_instance:function(any):string 25 | try_get_class_name_for_instance:function(any):string 26 | 27 | get_class_for_instance:function(any):any 28 | try_get_class_for_instance:function(any):any 29 | 30 | is_instance:function(obj:any, cls:any):boolean 31 | end 32 | 33 | return Class 34 | -------------------------------------------------------------------------------- /src/teal_language_server/class.lua: -------------------------------------------------------------------------------- 1 | 2 | local asserts = require("teal_language_server.asserts") 3 | 4 | local Class = {} 5 | 6 | function Class.try_get_name(cls) 7 | return cls.__name 8 | end 9 | 10 | function Class.get_name(cls) 11 | local name = Class.try_get_name(cls) 12 | if name == nil then 13 | error("Attempted to get class name for non-class type!") 14 | end 15 | return name 16 | end 17 | 18 | function Class.try_get_class_name_for_instance(instance) 19 | if instance == nil or type(instance) ~= "table" then 20 | return nil 21 | end 22 | local class = instance.__class 23 | if class == nil then 24 | return nil 25 | end 26 | return Class.try_get_name(class) 27 | end 28 | 29 | function Class.try_get_class_for_instance(obj) 30 | return obj.__class 31 | end 32 | 33 | function Class.get_class_for_instance(obj) 34 | local cls = Class.try_get_class_for_instance(obj) 35 | if cls == nil then 36 | error("Attempted to get class for non-class type!") 37 | end 38 | return cls 39 | end 40 | 41 | function Class.is_instance(obj, cls) 42 | return obj.__class == cls 43 | end 44 | 45 | function Class.get_class_name_for_instance(instance) 46 | local name = Class.try_get_class_name_for_instance(instance) 47 | if name == nil then 48 | error("Attempted to get class name for non-class type!") 49 | end 50 | return name 51 | end 52 | 53 | function Class.setup(class, class_name, options) 54 | class.__name = class_name 55 | 56 | options = options or {} 57 | 58 | -- This is useful sometimes to verify that a given table represents a class 59 | class.__is_class = true 60 | 61 | if options.attributes ~= nil then 62 | class._attributes = options.attributes 63 | end 64 | 65 | if options.interfaces ~= nil then 66 | class._interfaces = options.interfaces 67 | end 68 | 69 | if options.getters then 70 | for k, v in pairs(options.getters) do 71 | if type(v) == "string" then 72 | asserts.that(class[v] ~= nil, "Found getter property '{}' mapped to non-existent method '{}' for class '{}'", k, v, class_name) 73 | end 74 | end 75 | end 76 | 77 | local nilable_members = {} 78 | 79 | if options.nilable_members ~= nil then 80 | for _, value in ipairs(options.nilable_members) do 81 | nilable_members[value] = true 82 | end 83 | end 84 | 85 | if options.setters then 86 | for k, v in pairs(options.setters) do 87 | if type(v) == "string" then 88 | asserts.that(class[v] ~= nil, "Found setter property '{}' mapped to non-existent method '{}' for class '{}'", k, v, class_name) 89 | end 90 | end 91 | end 92 | 93 | -- Assume closed by default 94 | local is_closed = true 95 | 96 | if options.closed ~= nil and not options.closed then 97 | is_closed = false 98 | end 99 | 100 | local is_immutable = false 101 | 102 | if options.immutable ~= nil and options.immutable then 103 | is_immutable = true 104 | end 105 | 106 | if is_immutable then 107 | asserts.that(is_closed, "Attempted to create a non-closed immutable class '{}'. This is not allowed", class_name) 108 | end 109 | 110 | local function create_immutable_wrapper(t, class_name) 111 | local proxy = {} 112 | local mt = { 113 | __index = t, 114 | __newindex = function(t, k, v) 115 | asserts.fail("Attempted to change field '{}' of immutable class '{}'", k, class_name) 116 | end, 117 | __len = function() 118 | return #t 119 | end, 120 | __pairs = function() 121 | return pairs(t) 122 | end, 123 | __ipairs = function() 124 | return ipairs(t) 125 | end, 126 | __tostring = function() 127 | return tostring(t) 128 | end 129 | } 130 | setmetatable(proxy, mt) 131 | return proxy 132 | end 133 | 134 | setmetatable( 135 | class, { 136 | __call = function(_, ...) 137 | local mt = {} 138 | local instance = setmetatable({ __class = class }, mt) 139 | 140 | -- We need to call __init before defining __newindex below 141 | -- This is also nice because all classes are required to define 142 | -- default values for all their members in __init 143 | if class.__init ~= nil then 144 | class.__init(instance, ...) 145 | end 146 | 147 | local tostring_handler = class["__tostring"] 148 | if tostring_handler ~= nil then 149 | mt.__tostring = tostring_handler 150 | end 151 | 152 | mt.__index = function(_, k) 153 | if options.getters then 154 | local getter_value = options.getters[k] 155 | if getter_value then 156 | if type(getter_value) == "string" then 157 | return class[getter_value](instance) 158 | end 159 | 160 | return getter_value(instance) 161 | end 162 | end 163 | 164 | local static_member = class[k] 165 | if is_closed then 166 | -- This check means that member values cannot ever be set to nil 167 | -- So we provide the closed flag to allow for this case 168 | asserts.that(static_member ~= nil or nilable_members[k] ~= nil, "Attempted to get non-existent member '{}' on class '{}'. If its valid for the class to have nil members, then pass 'closed=false' to class.setup", k, class_name) 169 | end 170 | return static_member 171 | end 172 | 173 | mt.__newindex = function(_, k, value) 174 | if is_closed and nilable_members[k] == nil then 175 | asserts.that(options.setters, "Attempted to set non-existent property '{}' on class '{}'", k, class_name) 176 | 177 | local setter_value = options.setters[k] 178 | asserts.that(setter_value, "Attempted to set non-existent property '{}' on class '{}'", k, class_name) 179 | 180 | if type(setter_value) == "string" then 181 | rawget(class, setter_value)(instance, value) 182 | else 183 | setter_value(instance, value) 184 | end 185 | else 186 | asserts.that(not is_immutable) 187 | rawset(instance, k, value) 188 | end 189 | end 190 | 191 | if is_immutable then 192 | return create_immutable_wrapper(instance, class_name) 193 | end 194 | 195 | return instance 196 | end, 197 | } 198 | ) 199 | end 200 | 201 | return Class 202 | -------------------------------------------------------------------------------- /src/teal_language_server/constants.tl: -------------------------------------------------------------------------------- 1 | 2 | local record constants 3 | end 4 | 5 | return constants 6 | -------------------------------------------------------------------------------- /src/teal_language_server/document_manager.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "document_manager" 2 | 3 | -- 4 | local ServerState = require("teal_language_server.server_state") 5 | local LspReaderWriter = require("teal_language_server.lsp_reader_writer") 6 | local Uri = require("teal_language_server.uri") 7 | local Document = require("teal_language_server.document") 8 | local asserts = require("teal_language_server.asserts") 9 | local class = require("teal_language_server.class") 10 | 11 | local record DocumentManager 12 | docs:{string:Document} 13 | 14 | _docs:{string:Document} 15 | _lsp_reader_writer: LspReaderWriter 16 | _server_state: ServerState 17 | 18 | metamethod __call: function(self: DocumentManager, lsp_reader_writer: LspReaderWriter, server_state: ServerState): DocumentManager 19 | end 20 | 21 | function DocumentManager:__init(lsp_reader_writer: LspReaderWriter, server_state: ServerState) 22 | asserts.is_not_nil(lsp_reader_writer) 23 | asserts.is_not_nil(server_state) 24 | 25 | self._docs = {} 26 | self._lsp_reader_writer = lsp_reader_writer 27 | self._server_state = server_state 28 | end 29 | 30 | function DocumentManager:open(uri: Uri, content: string, version: integer):Document 31 | asserts.that(self._docs[uri.path] == nil) 32 | local doc = Document(uri, content, version, self._lsp_reader_writer, self._server_state) 33 | self._docs[uri.path] = doc 34 | return doc 35 | end 36 | 37 | function DocumentManager:close(uri: Uri) 38 | asserts.that(self._docs[uri.path] ~= nil) 39 | self._docs[uri.path] = nil 40 | end 41 | 42 | function DocumentManager:get(uri: Uri):Document 43 | return self._docs[uri.path] 44 | end 45 | 46 | class.setup(DocumentManager, "DocumentManager", { 47 | getters = { 48 | docs = function(self:DocumentManager):{string:Document} 49 | return self._docs 50 | end 51 | } 52 | }) 53 | return DocumentManager 54 | -------------------------------------------------------------------------------- /src/teal_language_server/env_updater.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "env_updater" 2 | 3 | -- 4 | local DocumentManager = require("teal_language_server.document_manager") 5 | local lusc = require("lusc") 6 | local ServerState = require("teal_language_server.server_state") 7 | local tl = require("tl") 8 | local TealProjectConfig = require("teal_language_server.teal_project_config") 9 | local uv = require("luv") 10 | local asserts = require("teal_language_server.asserts") 11 | local tracing = require("teal_language_server.tracing") 12 | local class = require("teal_language_server.class") 13 | 14 | local init_path = package.path 15 | local init_cpath = package.cpath 16 | 17 | local record EnvUpdater 18 | _document_manager: DocumentManager 19 | _server_state: ServerState 20 | _substitutions: {string:string} 21 | _root_nursery:lusc.Nursery 22 | _change_detected: lusc.StickyEvent 23 | 24 | metamethod __call: function(self: EnvUpdater, server_state: ServerState, root_nursery:lusc.Nursery, document_manager: DocumentManager): EnvUpdater 25 | end 26 | 27 | function EnvUpdater:__init(server_state: ServerState, root_nursery:lusc.Nursery, document_manager: DocumentManager) 28 | asserts.is_not_nil(document_manager) 29 | 30 | self._change_detected = lusc.new_sticky_event() 31 | self._server_state = server_state 32 | self._substitutions = {} 33 | self._root_nursery = root_nursery 34 | self._document_manager = document_manager 35 | end 36 | 37 | function EnvUpdater:_init_env_from_config(cfg: TealProjectConfig): tl.Env, string 38 | local function ivalues(t: {any:Value}): function(): Value 39 | local i = 0 40 | return function(): Value 41 | i = i + 1 42 | return t[i] 43 | end 44 | end 45 | 46 | local path_separator = package.config:sub(1, 1) 47 | local shared_lib_ext = package.cpath:match("(%.%w+)%s*$") or ".so" 48 | 49 | local function prepend_to_lua_path(path_str: string) 50 | if path_str:sub(-1) == path_separator then 51 | path_str = path_str:sub(1, -2) 52 | end 53 | 54 | path_str = path_str .. path_separator 55 | 56 | package.path = path_str .. "?.lua;" 57 | .. path_str .. "?" .. path_separator .. "init.lua;" 58 | .. package.path 59 | 60 | package.cpath = path_str .. "?." .. shared_lib_ext .. ";" 61 | .. package.cpath 62 | end 63 | 64 | local function esc_char(c: string): string 65 | return "%" .. c 66 | end 67 | 68 | local function str_esc(s: string, sub?: string | function(string): string | {string:string}): string, integer 69 | return s:gsub( 70 | "[%^%$%(%)%%%.%[%]%*%+%-%?]", 71 | sub as function(string): string 72 | or esc_char 73 | ) 74 | end 75 | 76 | local function add_module_substitute(source_dir: string, mod_name: string) 77 | self._substitutions[source_dir] = "^" .. str_esc(mod_name) 78 | end 79 | 80 | local function init_teal_env(gen_compat: tl.GenCompat, gen_target: tl.GenTarget, env_def: string): tl.Env, string 81 | local opts:tl.EnvOptions = { 82 | defaults = { 83 | gen_compat = gen_compat, 84 | gen_target = gen_target, 85 | }, 86 | predefined_modules = {env_def}, 87 | } 88 | 89 | local env = tl.new_env(opts) 90 | env.report_types = true 91 | return env 92 | end 93 | 94 | cfg = cfg or {} 95 | 96 | for dir in ivalues(cfg.include_dir or {}) do 97 | prepend_to_lua_path(dir) 98 | end 99 | 100 | if cfg.source_dir and cfg.module_name then 101 | add_module_substitute(cfg.source_dir, cfg.module_name) 102 | end 103 | 104 | local env, err = init_teal_env(cfg.gen_compat, cfg.gen_target, cfg.global_env_def) 105 | if not env then 106 | return nil, err 107 | end 108 | 109 | return env 110 | end 111 | 112 | function EnvUpdater:_generate_env(): tl.Env 113 | local config = self._server_state.config 114 | asserts.is_not_nil(config) 115 | 116 | -- applying the config to the env adds to package.path 117 | -- so lets reset them before doing that 118 | package.path = init_path 119 | package.cpath = init_cpath 120 | 121 | local env, errs = self:_init_env_from_config(config) 122 | 123 | if errs ~= nil and #errs > 0 then 124 | tracing.debug(_module_name, "Loaded env with errors:\n{}", {errs}) 125 | end 126 | 127 | return env 128 | end 129 | 130 | function EnvUpdater:_update_env_on_changes() 131 | local required_delay_without_saves_sec = 0.1 132 | 133 | while true do 134 | self._change_detected:await() 135 | self._change_detected:unset() 136 | 137 | -- Full env updates can be costly for large projects, and it is common for many 138 | -- documents to be saved all at once, so delay slightly so we just perform one 139 | -- env update 140 | while true do 141 | lusc.await_sleep(required_delay_without_saves_sec) 142 | if self._change_detected.is_set then 143 | tracing.debug(_module_name, "Detected consecutive change events, waiting again...", {}) 144 | self._change_detected:unset() 145 | else 146 | tracing.debug(_module_name, "Successfully waited for buffer time. Now updating env...", {}) 147 | break 148 | end 149 | end 150 | 151 | tracing.debug(_module_name, "Now updating env...", {}) 152 | local start_time = uv.hrtime() 153 | local env = self:_generate_env() 154 | self._server_state:set_env(env) 155 | local elapsed_time_ms = (uv.hrtime() - start_time) / 1e6 156 | tracing.debug(_module_name, "Completed env update in {} ms", {elapsed_time_ms}) 157 | 158 | for _, doc in pairs(self._document_manager.docs) do 159 | doc:clear_cache() 160 | doc:process_and_publish_results() 161 | end 162 | end 163 | end 164 | 165 | function EnvUpdater:schedule_env_update() 166 | self._change_detected:set() 167 | end 168 | 169 | function EnvUpdater:initialize() 170 | local env = self:_generate_env() 171 | self._server_state:set_env(env) 172 | 173 | self._root_nursery:start_soon(function() 174 | self:_update_env_on_changes() 175 | end) 176 | end 177 | 178 | class.setup(EnvUpdater, "EnvUpdater") 179 | return EnvUpdater 180 | -------------------------------------------------------------------------------- /src/teal_language_server/lsp.tl: -------------------------------------------------------------------------------- 1 | 2 | -- Type describing the actual protocol 3 | -- most of these are adapted from the typescript types the spec gives at 4 | -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/ 5 | 6 | local tl = require("tl") 7 | 8 | local record lsp 9 | 10 | enum ErrorName 11 | "InternalError" 12 | "InvalidParams" 13 | "InvalidRequest" 14 | "MethodNotFound" 15 | "ParseError" 16 | "ServerNotInitialized" 17 | "UnknownErrorCode" 18 | "serverErrorEnd" 19 | "serverErrorStart" 20 | 21 | "RequestCancelled" 22 | end 23 | 24 | error_code: {ErrorName:integer} 25 | 26 | type JsonValue = string | number | boolean | JsonObject -- should also have {JsonValue} 27 | type JsonObject = {string:JsonValue} 28 | 29 | type uinteger = integer 30 | 31 | record Message 32 | id: integer | string 33 | 34 | -- Request/Notification 35 | method: string 36 | params: {string:any} -- should be {any} | {string:any} 37 | 38 | -- Response 39 | result: JsonValue 40 | record ResponseError 41 | code: integer 42 | message: string 43 | data: JsonValue 44 | end 45 | error: ResponseError 46 | end 47 | 48 | record Position 49 | -- Both of these are 0 based 50 | line: uinteger 51 | character: uinteger 52 | end 53 | 54 | record Range 55 | start: Position 56 | ["end"]: Position 57 | end 58 | 59 | record Location 60 | uri: string 61 | range: Range 62 | end 63 | 64 | enum Severity 65 | "Error" 66 | "Warning" 67 | "Information" 68 | "Hint" 69 | end 70 | severity: {Severity:integer} 71 | 72 | record Diagnostic 73 | range: Range 74 | severity: integer 75 | message: string 76 | end 77 | 78 | record Method 79 | enum Name 80 | "initialize" 81 | "initialized" 82 | "textDocument/didOpen" 83 | "textDocument/didChange" 84 | "textDocument/didSave" 85 | "textDocument/didClose" 86 | "textDocument/hover" 87 | "textDocument/definition" 88 | "textDocument/publishDiagnostics" 89 | "textDocument/completion" 90 | "textDocument/signatureHelp" 91 | "shutdown" 92 | end 93 | 94 | type Params = {integer|string:JsonValue} -- should be JsonObject | {JsonValue} 95 | type Method = function(Params, integer) 96 | end 97 | 98 | record TextDocument 99 | uri: string 100 | version: integer 101 | text: string 102 | languageId: string 103 | end 104 | 105 | enum TextDocumentSyncKind 106 | "None" 107 | "Full" 108 | "Incremental" 109 | end 110 | sync_kind: {TextDocumentSyncKind:integer} 111 | 112 | record TextDocumentContentChangeEvent 113 | range: Range 114 | text: string 115 | end 116 | 117 | enum CompletionTriggerKind 118 | "Invoked" 119 | "TriggerCharacter" 120 | "TriggerForIncompleteCompletions" 121 | end 122 | completion_trigger_kind: {CompletionTriggerKind:integer} 123 | 124 | enum CompletionItemKind 125 | "Text" 126 | "Method" 127 | "Function" 128 | "Constructor" 129 | "Field" 130 | "Variable" 131 | "Class" 132 | "Interface" 133 | "Module" 134 | "Property" 135 | "Unit" 136 | "Value" 137 | "Enum" 138 | "Keyword" 139 | "Snippet" 140 | "Color" 141 | "File" 142 | "Reference" 143 | "Folder" 144 | "EnumMember" 145 | "Constant" 146 | "Struct" 147 | "Event" 148 | "Operator" 149 | "TypeParameter" 150 | end 151 | completion_item_kind: {CompletionItemKind:integer} 152 | typecodes_to_kind: {integer:integer} 153 | 154 | record CompletionContext 155 | triggerKind: integer 156 | triggerCharacter: string 157 | end 158 | 159 | end 160 | 161 | lsp.error_code = { 162 | InternalError = -32603, 163 | InvalidParams = -32602, 164 | InvalidRequest = -32600, 165 | MethodNotFound = -32601, 166 | ParseError = -32700, 167 | ServerNotInitialized = -32002, 168 | UnknownErrorCode = -32001, 169 | serverErrorEnd = -32000, 170 | serverErrorStart = -32099, 171 | 172 | RequestCancelled = -32800, 173 | } 174 | 175 | lsp.severity = { 176 | Error = 1, 177 | Warning = 2, 178 | Information = 3, 179 | Hint = 4, 180 | } 181 | 182 | lsp.sync_kind = { 183 | None = 0, 184 | Full = 1, 185 | Incremental = 2, 186 | } 187 | 188 | lsp.completion_trigger_kind = { 189 | Invoked = 1, 190 | TriggerCharacter = 2, 191 | TriggerForIncompleteCompletions = 3, 192 | } 193 | 194 | lsp.completion_item_kind = { 195 | Text = 1, 196 | Method = 2, 197 | Function = 3, 198 | Constructor = 4, 199 | Field = 5, 200 | Variable = 6, 201 | Class = 7, 202 | Interface = 8, 203 | Module = 9, 204 | Property = 10, 205 | Unit = 11, 206 | Value = 12, 207 | Enum = 13, 208 | Keyword = 14, 209 | Snippet = 15, 210 | Color = 16, 211 | File = 17, 212 | Reference = 18, 213 | Folder = 19, 214 | EnumMember = 20, 215 | Constant = 21, 216 | Struct = 22, 217 | Event = 23, 218 | Operator = 24, 219 | TypeParameter = 25, 220 | } 221 | 222 | -- maybe could be moved to a different file or something. 223 | -- it's one of the few that does a tl mapping to something outside of teal. 224 | lsp.typecodes_to_kind = { 225 | -- Lua types 226 | [tl.typecodes.NIL] = lsp.completion_item_kind.Variable, 227 | [tl.typecodes.NUMBER] = lsp.completion_item_kind.Variable, 228 | [tl.typecodes.BOOLEAN] = lsp.completion_item_kind.Variable, 229 | [tl.typecodes.STRING] = lsp.completion_item_kind.Variable, 230 | [tl.typecodes.TABLE] = lsp.completion_item_kind.Struct, 231 | [tl.typecodes.FUNCTION] = lsp.completion_item_kind.Function, 232 | [tl.typecodes.USERDATA] = lsp.completion_item_kind.Variable, 233 | [tl.typecodes.THREAD] = lsp.completion_item_kind.Variable, 234 | -- -- Teal types 235 | [tl.typecodes.INTEGER] = lsp.completion_item_kind.Variable, 236 | [tl.typecodes.ENUM] = lsp.completion_item_kind.Enum, 237 | [tl.typecodes.ARRAY] = lsp.completion_item_kind.Struct, 238 | [tl.typecodes.RECORD] = lsp.completion_item_kind.Reference, 239 | [tl.typecodes.MAP] = lsp.completion_item_kind.Struct, 240 | [tl.typecodes.TUPLE] = lsp.completion_item_kind.Struct, 241 | [tl.typecodes.INTERFACE] = lsp.completion_item_kind.Interface , 242 | [tl.typecodes.SELF] = lsp.completion_item_kind.Struct, 243 | [tl.typecodes.POLY] = lsp.completion_item_kind.Function, 244 | [tl.typecodes.UNION] = lsp.completion_item_kind.TypeParameter, 245 | -- -- Indirect types 246 | [tl.typecodes.NOMINAL] = lsp.completion_item_kind.Variable, 247 | [tl.typecodes.TYPE_VARIABLE] = lsp.completion_item_kind.Reference, 248 | -- -- Special types 249 | [tl.typecodes.ANY] = lsp.completion_item_kind.Variable, 250 | [tl.typecodes.UNKNOWN] = lsp.completion_item_kind.Variable, 251 | [tl.typecodes.INVALID] = lsp.completion_item_kind.Text, 252 | } 253 | 254 | function lsp.position(y: lsp.uinteger, x: lsp.uinteger): lsp.Position 255 | return { 256 | character = x, 257 | line = y, 258 | } 259 | end 260 | 261 | return lsp 262 | 263 | -------------------------------------------------------------------------------- /src/teal_language_server/lsp_events_manager.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "lsp_events_manager" 2 | 3 | local lsp = require("teal_language_server.lsp") 4 | local LspReaderWriter = require("teal_language_server.lsp_reader_writer") 5 | local lusc = require("lusc") 6 | local asserts = require("teal_language_server.asserts") 7 | local tracing = require("teal_language_server.tracing") 8 | local class = require("teal_language_server.class") 9 | 10 | local record LspEventsManager 11 | _lsp_reader_writer: LspReaderWriter 12 | _root_nursery:lusc.Nursery 13 | 14 | _handlers:{lsp.Method.Name: function(lsp.Method.Params, integer)} 15 | 16 | metamethod __call: function(self: LspEventsManager, root_nursery:lusc.Nursery, lsp_reader_writer:LspReaderWriter): LspEventsManager 17 | end 18 | 19 | function LspEventsManager:__init(root_nursery:lusc.Nursery, lsp_reader_writer:LspReaderWriter) 20 | asserts.is_not_nil(root_nursery) 21 | asserts.is_not_nil(lsp_reader_writer) 22 | 23 | self._handlers = {} 24 | self._lsp_reader_writer = lsp_reader_writer 25 | self._root_nursery = root_nursery 26 | end 27 | 28 | function LspEventsManager:set_handler(method:lsp.Method.Name, handler:function(lsp.Method.Params, integer)) 29 | asserts.that(self._handlers[method] == nil) 30 | self._handlers[method] = handler 31 | end 32 | 33 | function LspEventsManager:_trigger(method:lsp.Method.Name, params:lsp.Method.Params, id:integer) 34 | tracing.info(_module_name, "Received request from client for method {}", {method}) 35 | 36 | if self._handlers[method] then 37 | local ok: boolean 38 | local err: string 39 | 40 | ok, err = xpcall( 41 | function() self._handlers[method](params, id) end, 42 | debug.traceback as function) as (boolean, string) 43 | 44 | if ok then 45 | tracing.debug(_module_name, "Successfully handled request with method {}", {method}) 46 | else 47 | tracing.error(_module_name, "Error in handler for request with method {}: {}", {method, err}) 48 | end 49 | else 50 | tracing.warning(_module_name, "No handler found for event with method {}", {method}) 51 | end 52 | end 53 | 54 | function LspEventsManager:_receive_initialize_request() 55 | local initialize_data = self._lsp_reader_writer:receive_rpc() 56 | 57 | asserts.is_not_nil(initialize_data) 58 | 59 | asserts.that(initialize_data.method ~= nil, "No method in initial request") 60 | asserts.that(initialize_data.method == "initialize", "Initial method was not 'initialize'") 61 | 62 | tracing.trace(_module_name, "Received initialize request from client with data: {}", {initialize_data}) 63 | 64 | self:_trigger( 65 | "initialize", initialize_data.params as lsp.Method.Params, initialize_data.id as integer) 66 | end 67 | 68 | function LspEventsManager:initialize() 69 | self._root_nursery:start_soon(function() 70 | -- Initialize method must be first 71 | self:_receive_initialize_request() 72 | 73 | while true do 74 | local data = self._lsp_reader_writer:receive_rpc() 75 | asserts.is_not_nil(data) 76 | asserts.is_not_nil(data.method) 77 | 78 | self:_trigger( 79 | data.method as lsp.Method.Name, data.params as lsp.Method.Params, data.id as integer) 80 | end 81 | end) 82 | end 83 | 84 | class.setup(LspEventsManager, "LspEventsManager") 85 | return LspEventsManager 86 | -------------------------------------------------------------------------------- /src/teal_language_server/lsp_formatter.tl: -------------------------------------------------------------------------------- 1 | local tl = require("tl") 2 | 3 | local Document = require("teal_language_server.document") 4 | 5 | local record lsp_formatter 6 | record Documentation 7 | kind: string 8 | value: string 9 | end 10 | 11 | record SignatureHelp 12 | record SignatureParameter 13 | label: {integer, integer} 14 | documentation: Documentation 15 | end 16 | 17 | record Signature 18 | label: string 19 | parameters: {SignatureParameter} 20 | documentation: Documentation 21 | activeParameter: integer 22 | end 23 | 24 | signatures: {Signature} 25 | activeSignature: integer 26 | activeParameter: integer 27 | end 28 | end 29 | 30 | local function _split_not_in_parenthesis(str: string, start: integer, finish: integer): {string} 31 | local parens_count = 0 32 | local i = start 33 | local output = {} 34 | local start_field = i 35 | while i <= finish do 36 | if str:sub(i, i) == "(" then 37 | parens_count = parens_count + 1 38 | end 39 | if str:sub(i, i) == ")" then 40 | parens_count = parens_count - 1 41 | end 42 | if str:sub(i, i) == "," and parens_count == 0 then 43 | output[#output + 1] = str:sub(start_field, i) 44 | start_field = i + 2 45 | end 46 | i = i + 1 47 | end 48 | table.insert(output, str:sub(start_field, i)) 49 | return output 50 | end 51 | 52 | function lsp_formatter.create_function_string(type_string: string, arg_names: {string}, tk?: string): string 53 | local _, _, types, args, returns = type_string:find("^function(.-)(%b())(.-)$") as (integer, integer, string, string, string) 54 | local output: {string} = {} 55 | if tk then output[1] = tk else output[1] = "function" end 56 | output[2] = types 57 | output[3] = "(" 58 | 59 | for i, argument in ipairs(_split_not_in_parenthesis(args, 2, #args-2)) do 60 | output[#output+1] = arg_names[i] 61 | output[#output+1] = ": " 62 | output[#output+1] = argument 63 | output[#output+1] = " " 64 | end 65 | output[#output] = ")" 66 | output[#output+1] = returns 67 | return table.concat(output) 68 | end 69 | 70 | local record StringBuilder 71 | strings: {string} 72 | 73 | build: function(self): string = macroexp(self: StringBuilder): string 74 | return table.concat(self.strings, "\n") 75 | end 76 | 77 | add: function(self, line: string): nil = macroexp(self: StringBuilder, line: string): nil 78 | return table.insert(self.strings, line) 79 | end 80 | end 81 | 82 | function lsp_formatter.show_type(node_info: Document.NodeInfo, type_info: tl.TypeInfo, doc: Document): lsp_formatter.Documentation 83 | local output: lsp_formatter.Documentation = {kind="markdown"} 84 | local sb: StringBuilder = {strings = {}} 85 | sb:add("```teal") 86 | 87 | if type_info.t == tl.typecodes.FUNCTION then 88 | local args = doc:get_function_args_string(type_info) 89 | if args ~= nil then 90 | sb:add("function " .. lsp_formatter.create_function_string(type_info.str, args, node_info.source)) 91 | else 92 | sb:add(node_info.source .. ": " .. type_info.str) 93 | end 94 | 95 | elseif type_info.t == tl.typecodes.POLY then 96 | for i, type_ref in ipairs(type_info.types) do 97 | local func_info = doc:resolve_type_ref(type_ref) 98 | local args = doc:get_function_args_string(func_info) 99 | if args ~= nil then 100 | sb:add("function " .. lsp_formatter.create_function_string(func_info.str, args, node_info.source)) 101 | else 102 | local replaced_function = func_info.str:gsub("^function", node_info.source) 103 | sb:add(replaced_function) 104 | end 105 | if i < #type_info.types then 106 | sb:add("```") 107 | sb:add("or") 108 | sb:add("```teal") 109 | end 110 | end 111 | 112 | elseif type_info.t == tl.typecodes.ENUM then 113 | sb:add("enum " .. type_info.str) 114 | for _, _enum in ipairs(type_info.enums) do 115 | sb:add(' "' .. _enum .. '"') 116 | end 117 | sb:add("end") 118 | 119 | elseif type_info.t == tl.typecodes.RECORD then 120 | sb:add("record " .. type_info.str) 121 | for key, type_ref in pairs(type_info.fields) do 122 | local type_ref_info = doc:resolve_type_ref(type_ref) 123 | sb:add(' ' .. key .. ': ' .. type_ref_info.str) 124 | end 125 | sb:add("end") 126 | 127 | else 128 | sb:add(node_info.source .. ": " .. type_info.str) 129 | end 130 | sb:add("```") 131 | output.value = sb:build() 132 | return output 133 | end 134 | 135 | return lsp_formatter 136 | -------------------------------------------------------------------------------- /src/teal_language_server/lsp_reader_writer.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "lsp_reader_writer" 2 | 3 | local StdinReader = require("teal_language_server.stdin_reader") 4 | local lsp = require("teal_language_server.lsp") 5 | local json = require("cjson") 6 | local uv = require("luv") 7 | local asserts = require("teal_language_server.asserts") 8 | local tracing = require("teal_language_server.tracing") 9 | local class = require("teal_language_server.class") 10 | 11 | local record LspReaderWriter 12 | _stdin_reader:StdinReader 13 | _stdout: uv.Pipe 14 | _disposed: boolean 15 | 16 | metamethod __call: function(self: LspReaderWriter, stdin_reader: StdinReader): LspReaderWriter 17 | end 18 | 19 | function LspReaderWriter:__init(stdin_reader: StdinReader) 20 | asserts.is_not_nil(stdin_reader) 21 | self._stdin_reader = stdin_reader 22 | self._disposed = false 23 | end 24 | 25 | local record HeaderInfo 26 | length:integer 27 | content_type:string 28 | end 29 | 30 | local function json_nullable(x: T): T 31 | if x == nil then 32 | return json.null as T 33 | end 34 | return x 35 | end 36 | 37 | local contenttype: {string:boolean} = { 38 | ["application/vscode-jsonrpc; charset=utf8"] = true, 39 | ["application/vscode-jsonrpc; charset=utf-8"] = true, 40 | } 41 | 42 | function LspReaderWriter:_parse_header(lines:{string}):HeaderInfo 43 | local len:integer 44 | local content_type:string 45 | 46 | for _, line in ipairs(lines) do 47 | local key, val = line:match("^([^:]+): (.+)$") 48 | 49 | asserts.that(key ~= nil and val ~= nil, "invalid header: " .. line) 50 | 51 | tracing.trace(_module_name, "Request Header: {}: {}", {key, val}) 52 | 53 | if key == "Content-Length" then 54 | asserts.is_nil(len) 55 | len = tonumber(val) as integer 56 | elseif key == "Content-Type" then 57 | if contenttype[val] == nil then 58 | asserts.fail("Invalid Content-Type '{}'", val) 59 | end 60 | asserts.is_nil(content_type) 61 | content_type = val 62 | else 63 | asserts.fail("Unexpected header: {}", line) 64 | end 65 | end 66 | 67 | asserts.that(len ~= nil, "Missing Content-Length") 68 | 69 | return { 70 | length = len, 71 | content_type = content_type, 72 | } 73 | end 74 | 75 | function LspReaderWriter:initialize() 76 | self._stdout = uv.new_pipe(false) 77 | asserts.that(self._stdout ~= nil) 78 | assert(self._stdout:open(1)) 79 | tracing.trace(_module_name, "Opened pipe for stdout") 80 | end 81 | 82 | function LspReaderWriter:dispose() 83 | asserts.that(not self._disposed) 84 | self._disposed = true 85 | self._stdout:close() 86 | tracing.debug(_module_name, "Closed pipe for stdout") 87 | end 88 | 89 | function LspReaderWriter:_decode_header():HeaderInfo 90 | local header_lines:{string} = {} 91 | 92 | tracing.trace(_module_name, "Reading LSP rpc header...") 93 | while true do 94 | local header_line = self._stdin_reader:read_line() 95 | 96 | if #header_line == 0 then 97 | break 98 | end 99 | 100 | table.insert(header_lines, header_line) 101 | end 102 | 103 | return self:_parse_header(header_lines) 104 | end 105 | 106 | function LspReaderWriter:receive_rpc():{string:any} 107 | local header_info = self:_decode_header() 108 | 109 | tracing.trace(_module_name, "Successfully read LSP rpc header: {}\nWaiting to receive body...", {header_info}) 110 | local body_line = self._stdin_reader:read(header_info.length) 111 | tracing.trace(_module_name, "Received request Body: {}", {body_line}) 112 | 113 | local data = json.decode(body_line) 114 | 115 | asserts.that(data and type(data) == 'table', "Malformed json") 116 | asserts.that(data.jsonrpc == "2.0", "Incorrect jsonrpc version! Got {} but expected 2.0", data.jsonrpc) 117 | 118 | return data 119 | end 120 | 121 | function LspReaderWriter:_encode(t: {string:any}) 122 | assert(t.jsonrpc == "2.0", "Expected jsonrpc to be 2.0") 123 | 124 | local msg = json.encode(t) 125 | 126 | local content = "Content-Length: " .. tostring(#msg) .. "\r\n\r\n" .. msg 127 | assert(self._stdout:write(content)) 128 | 129 | tracing.trace(_module_name, "Sending data: {}", {content}) 130 | end 131 | 132 | function LspReaderWriter:send_rpc(id: integer, t: {string:any}) 133 | self:_encode { 134 | jsonrpc = "2.0", 135 | id = json_nullable(id), 136 | result = json_nullable(t), 137 | } 138 | end 139 | 140 | function LspReaderWriter:send_rpc_error(id: integer, name: lsp.ErrorName, msg: string, data: {string:any}) 141 | self:_encode { 142 | jsonrpc = "2.0", 143 | id = json_nullable(id), 144 | error = { 145 | code = lsp.error_code[name] or lsp.error_code.UnknownErrorCode, 146 | message = msg, 147 | data = data, 148 | }, 149 | } 150 | end 151 | 152 | function LspReaderWriter:send_rpc_notification(method: lsp.Method.Name, params: lsp.Method.Params) 153 | self:_encode { 154 | jsonrpc = "2.0", 155 | method = method, 156 | params = params, 157 | } 158 | end 159 | 160 | class.setup(LspReaderWriter, "LspReaderWriter", { 161 | nilable_members = { '_stdout' } 162 | }) 163 | return LspReaderWriter 164 | -------------------------------------------------------------------------------- /src/teal_language_server/main.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "main" 2 | 3 | -- 4 | local EnvUpdater = require("teal_language_server.env_updater") 5 | local DocumentManager = require("teal_language_server.document_manager") 6 | local ServerState = require("teal_language_server.server_state") 7 | local LspEventsManager = require("teal_language_server.lsp_events_manager") 8 | local lusc = require("lusc") 9 | local uv = require("luv") 10 | local TraceStream = require("teal_language_server.trace_stream") 11 | local args_parser = require("teal_language_server.args_parser") 12 | local MiscHandlers = require("teal_language_server.misc_handlers") 13 | local StdinReader = require("teal_language_server.stdin_reader") 14 | local LspReaderWriter = require("teal_language_server.lsp_reader_writer") 15 | local tracing = require("teal_language_server.tracing") 16 | local util = require("teal_language_server.util") 17 | local TraceEntry = require("teal_language_server.trace_entry") 18 | 19 | local record IDisposable 20 | dispose: function(IDisposable) 21 | end 22 | 23 | local function init_logging(verbose:boolean):TraceStream 24 | local trace_stream = TraceStream() 25 | trace_stream:initialize() 26 | 27 | tracing.add_stream(function(entry:TraceEntry) 28 | trace_stream:log_entry(entry) 29 | end) 30 | 31 | if verbose then 32 | tracing.set_min_level("TRACE") 33 | else 34 | tracing.set_min_level("INFO") 35 | end 36 | return trace_stream 37 | end 38 | 39 | local function main() 40 | -- Immediately listen to log events and cache until 41 | -- we set up our file logger 42 | local cached_entries:{TraceEntry} = {} 43 | tracing.add_stream(function(entry:TraceEntry) 44 | if cached_entries then 45 | table.insert(cached_entries, entry) 46 | end 47 | end) 48 | 49 | local args = args_parser.parse_args() 50 | 51 | local trace_stream:TraceStream 52 | 53 | if args.log_mode ~= "none" then 54 | trace_stream = init_logging(args.verbose) 55 | 56 | for _, entry in ipairs(cached_entries) do 57 | trace_stream:log_entry(entry) 58 | end 59 | 60 | -- Uncomment if lusc is suspected to have an issue 61 | -- lusc.set_log_handler(function(message:string) 62 | -- tracing.debug("lusc", message, {}) 63 | -- end) 64 | end 65 | 66 | cached_entries = nil 67 | 68 | tracing.info(_module_name, "Started new instance teal-language-server. Lua Version: {}. Platform: {}", {_VERSION, util.get_platform()}) 69 | tracing.info(_module_name, "Received command line args: {}", {args}) 70 | tracing.info(_module_name, "CWD = {}", {uv.cwd()}) 71 | 72 | local disposables:{IDisposable} 73 | 74 | local function initialize() 75 | tracing.debug(_module_name, "Running object graph construction phase...", {}) 76 | 77 | local root_nursery = lusc.get_root_nursery() 78 | local stdin_reader = StdinReader() 79 | local lsp_reader_writer = LspReaderWriter(stdin_reader) 80 | local lsp_events_manager = LspEventsManager(root_nursery, lsp_reader_writer) 81 | local server_state = ServerState() 82 | local document_manager = DocumentManager(lsp_reader_writer, server_state) 83 | local env_updater = EnvUpdater(server_state, root_nursery, document_manager) 84 | local misc_handlers = MiscHandlers(lsp_events_manager, lsp_reader_writer, server_state, document_manager, trace_stream, args, env_updater) 85 | 86 | tracing.debug(_module_name, "Running initialize phase...", {}) 87 | stdin_reader:initialize() 88 | lsp_reader_writer:initialize() 89 | lsp_events_manager:initialize() 90 | misc_handlers:initialize() 91 | 92 | lsp_events_manager:set_handler("shutdown", function() 93 | tracing.info(_module_name, "Received shutdown request from client. Cancelling all lusc tasks...", {}) 94 | root_nursery.cancel_scope:cancel() 95 | end) 96 | 97 | disposables = { 98 | stdin_reader, lsp_reader_writer 99 | } as {IDisposable} 100 | end 101 | 102 | local function dispose() 103 | tracing.info(_module_name, "Disposing...", {}) 104 | 105 | if disposables then 106 | for _, disposable in ipairs(disposables) do 107 | disposable:dispose() 108 | end 109 | end 110 | end 111 | 112 | local lusc_timer = uv.new_timer() 113 | lusc_timer:start(0, 0, function() 114 | tracing.trace(_module_name, "Received entry point call from luv") 115 | 116 | lusc.start { 117 | -- TODO - consider turning this off by default 118 | generate_debug_names = true, 119 | on_completed = function(err:lusc.ErrorGroup) 120 | if err ~= nil then 121 | tracing.error(_module_name, "Received on_completed request with error:\n{}", {err}) 122 | else 123 | tracing.info(_module_name, "Received on_completed request") 124 | end 125 | 126 | dispose() 127 | end, 128 | } 129 | 130 | lusc.schedule(function() 131 | tracing.trace(_module_name, "Received entry point call from lusc luv") 132 | initialize() 133 | end) 134 | 135 | -- Tell lusc to end once all tasks complete 136 | lusc.stop() 137 | end) 138 | 139 | local function run_luv():nil 140 | tracing.trace(_module_name, "Running luv event loop...") 141 | uv.run() 142 | tracing.trace(_module_name, "Luv event loop stopped") 143 | lusc_timer:close() 144 | 145 | uv.walk(function(handle:uv.Handle) 146 | if not handle:is_closing() then 147 | local handle_type = handle:get_type() 148 | tracing.warning(_module_name, "Found unclosed handle of type '{}', closing it.", {handle_type}) 149 | handle:close() 150 | end 151 | end) 152 | 153 | uv.run('nowait') 154 | 155 | if uv.loop_close() then 156 | tracing.info(_module_name, "luv event loop closed gracefully") 157 | else 158 | tracing.warning(_module_name, "Could not close luv event loop gracefully") 159 | end 160 | end 161 | 162 | util.try { 163 | action = run_luv, 164 | catch = function(err:string):nil 165 | tracing.error(_module_name, "Error: {}", {err}) 166 | error(err) 167 | end, 168 | } 169 | end 170 | 171 | main() 172 | 173 | -------------------------------------------------------------------------------- /src/teal_language_server/path.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "path" 2 | 3 | local asserts = require("teal_language_server.asserts") 4 | local class = require("teal_language_server.class") 5 | local util = require("teal_language_server.util") 6 | local tracing = require("teal_language_server.tracing") 7 | local uv = require("luv") 8 | 9 | local default_dir_permissions = tonumber('755', 8) 10 | 11 | local record Path 12 | record WriteTextOpts 13 | overwrite: boolean 14 | end 15 | 16 | -- We make these read-only properties to ensure our path is immutable 17 | value:string 18 | 19 | _value:string 20 | 21 | record CreateDirectoryArgs 22 | parents:boolean 23 | exist_ok:boolean 24 | end 25 | 26 | metamethod __call: function(self: Path, value:string): Path 27 | end 28 | 29 | function Path:__init(value:string) 30 | asserts.that(value is string) 31 | asserts.that(#value > 0, "Path must be non empty string") 32 | 33 | self._value = value 34 | end 35 | 36 | function Path:is_valid():boolean 37 | local result = self._value:find("[" .. util.string_escape_special_chars("<>\"|?*") .. "]") 38 | return result == nil 39 | end 40 | 41 | function Path:get_value():string 42 | return self._value 43 | end 44 | 45 | function Path:__tostring():string 46 | -- return string.format("Path('%s')", self._value) 47 | return self._value 48 | end 49 | 50 | local function get_path_separator():string 51 | if util.get_platform() == "windows" then 52 | return "\\" 53 | end 54 | 55 | return "/" 56 | end 57 | 58 | local function _join(left:string, right:string):string 59 | if right == "." then 60 | return left 61 | end 62 | 63 | local combinedPath = left 64 | local lastCharLeft = left:sub(-1) 65 | 66 | local firstCharRight = right:sub(1, 1) 67 | local hasBeginningSlash = firstCharRight == '/' or firstCharRight == '\\' 68 | 69 | if lastCharLeft ~= '/' and lastCharLeft ~= '\\' then 70 | if not hasBeginningSlash then 71 | combinedPath = combinedPath .. get_path_separator() 72 | end 73 | else 74 | if hasBeginningSlash then 75 | right = right:sub(2, #right) 76 | end 77 | end 78 | 79 | combinedPath = combinedPath .. right 80 | return combinedPath 81 | end 82 | 83 | function Path:join(...:string):Path 84 | local args = {...} 85 | local result = self._value 86 | 87 | for _, value in ipairs(args) do 88 | result = _join(result, value) 89 | end 90 | 91 | return Path(result) 92 | end 93 | 94 | function Path:is_absolute():boolean 95 | if util.get_platform() == "windows" then 96 | return self._value:match('^[a-zA-Z]:[\\/]') ~= nil 97 | end 98 | 99 | return util.string_starts_with(self._value, '/') 100 | end 101 | 102 | function Path:is_relative():boolean 103 | return not self:is_absolute() 104 | end 105 | 106 | local function _remove_trailing_seperator_if_exists(path:string):string 107 | local result = path:match('^(.*[^\\/])[\\/]*$') 108 | asserts.is_not_nil(result, "Failed when processing path '{}'", path) 109 | return result 110 | end 111 | 112 | local function array_from_iterator(itr:(function():T)):{T} 113 | local result = {} 114 | for value in itr do 115 | table.insert(result, value) 116 | end 117 | return result 118 | end 119 | 120 | function Path:get_parts():{string} 121 | if self._value == '/' then 122 | return {} 123 | end 124 | 125 | -- Remove the trailing seperator if it exists 126 | local fixed_value = _remove_trailing_seperator_if_exists(self._value) 127 | return array_from_iterator(string.gmatch(fixed_value, "([^\\/]+)")) 128 | end 129 | 130 | function Path:try_get_parent():Path 131 | if self._value == '/' then 132 | return nil 133 | end 134 | 135 | -- Remove the trailing seperator if it exists 136 | local temp_path = _remove_trailing_seperator_if_exists(self._value) 137 | 138 | -- If we have no seperators then there is no parent 139 | -- This works for both windows and linux since on windows temp_path is C: which returns nil 140 | if not temp_path:match('[\\/]') then 141 | return nil 142 | end 143 | 144 | -- We remove the trailing slash here because this is more likely 145 | -- to be the canonical form 146 | local parent_path_str = temp_path:match('^(.*)[\\/][^\\/]*$') 147 | 148 | if util.get_platform() ~= 'windows' and #parent_path_str == 0 then 149 | parent_path_str = "/" 150 | end 151 | 152 | return Path(parent_path_str) 153 | end 154 | 155 | function Path:get_parent():Path 156 | local result = self:try_get_parent() 157 | asserts.is_not_nil(result, "Expected to find parent but none was found for path '{}'", self._value) 158 | return result 159 | end 160 | 161 | function Path:get_parents():{Path} 162 | local result:{Path} = {} 163 | local parent = self:try_get_parent() 164 | if not parent then 165 | return result 166 | end 167 | table.insert(result, parent) 168 | parent = parent:try_get_parent() 169 | while parent do 170 | table.insert(result, parent) 171 | parent = parent:try_get_parent() 172 | end 173 | return result 174 | end 175 | 176 | function Path:get_file_name():string 177 | if self._value == "/" then 178 | return "" 179 | end 180 | 181 | local path = _remove_trailing_seperator_if_exists(self._value) 182 | 183 | if not path:match('[\\/]') then 184 | return path 185 | end 186 | 187 | return path:match('[\\/]([^\\/]*)$') 188 | end 189 | 190 | function Path:get_extension():string 191 | local result = self._value:match('%.([^%.]*)$') 192 | return result 193 | end 194 | 195 | function Path:get_file_name_without_extension():string 196 | local fileName = self:get_file_name() 197 | local extension = self:get_extension() 198 | 199 | if extension == nil then 200 | return fileName 201 | end 202 | 203 | return fileName:sub(0, #fileName - #extension - 1) 204 | end 205 | 206 | function Path:is_directory():boolean 207 | local stats = uv.fs_stat(self._value) 208 | return stats ~= nil and stats.type == "directory" 209 | end 210 | 211 | function Path:is_file():boolean 212 | local stats = uv.fs_stat(self._value) 213 | return stats ~= nil and stats.type == "file" 214 | end 215 | 216 | function Path:delete_empty_directory() 217 | asserts.that(self:is_directory()) 218 | 219 | local success, error_message = pcall(uv.fs_rmdir, self._value) 220 | if not success then 221 | error(string.format("Failed to remove directory at '%s'. Details: %s", self._value, error_message)) 222 | end 223 | end 224 | 225 | function Path:get_sub_paths():{Path} 226 | asserts.that(self:is_directory(), "Attempted to get sub paths for non directory path '{}'", self._value) 227 | 228 | local req, err = uv.fs_scandir(self._value) 229 | if req == nil then 230 | error(string.format("Failed to open dir '%s' for scanning. Details: '%s'", self._value, err)) 231 | end 232 | 233 | local function iter():string, string 234 | local r1, r2 = uv.fs_scandir_next(req) 235 | -- fs_scandir_next returns nil when its complete, but it also returns nil on failure, 236 | -- and then passes the error as second return value 237 | if not (r1 ~= nil or (r1 == nil and r2 == nil)) then 238 | error(string.format("Failure while scanning directory '%s': %s", self._value, r2)) 239 | end 240 | return r1, r2 241 | end 242 | 243 | local result:{Path} = {} 244 | 245 | for name, _ in iter do 246 | table.insert(result, self:join(name)) 247 | end 248 | 249 | return result 250 | end 251 | 252 | function Path:get_sub_directories():{Path} 253 | local result:{Path} = {} 254 | 255 | for _, sub_path in ipairs(self:get_sub_paths()) do 256 | if sub_path:is_directory() then 257 | table.insert(result, sub_path) 258 | end 259 | end 260 | 261 | return result 262 | end 263 | 264 | function Path:get_sub_files():{Path} 265 | local result:{Path} = {} 266 | 267 | for _, sub_path in ipairs(self:get_sub_paths()) do 268 | if sub_path:is_file() then 269 | table.insert(result, sub_path) 270 | end 271 | end 272 | 273 | return result 274 | end 275 | 276 | function Path:exists():boolean 277 | local stats = uv.fs_stat(self._value) 278 | return stats ~= nil 279 | end 280 | 281 | function Path:delete_file() 282 | asserts.that(self:is_file(), "Called delete_file for non-file at path '{}'", self._value) 283 | assert(uv.fs_unlink(self._value)) 284 | tracing.trace(_module_name, "Deleted file at path '{}'", {self._value}) 285 | end 286 | 287 | function Path:create_directory(args?:Path.CreateDirectoryArgs) 288 | if args and args.exist_ok and self:exists() then 289 | asserts.that(self:is_directory()) 290 | return 291 | end 292 | 293 | if args and args.parents then 294 | local parent = self:try_get_parent() 295 | 296 | if not parent:exists() then 297 | parent:create_directory(args) 298 | end 299 | end 300 | 301 | local success, err = uv.fs_mkdir(self._value, default_dir_permissions) 302 | if not success then 303 | error(string.format("Failed to create directory '%s': %s", self._value, err)) 304 | end 305 | end 306 | 307 | class.setup(Path, "Path", { 308 | getters = { 309 | value = "get_value", 310 | }, 311 | }) 312 | 313 | function Path.cwd():Path 314 | local cwd, err = uv.cwd() 315 | if cwd == nil then 316 | error(string.format("Failed to obtain current directory: %s", err)) 317 | end 318 | return Path(cwd) 319 | end 320 | 321 | return Path 322 | 323 | -------------------------------------------------------------------------------- /src/teal_language_server/server_state.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "server_state" 2 | 3 | -- 4 | local asserts = require("teal_language_server.asserts") 5 | local lsp = require("teal_language_server.lsp") 6 | local Path = require("teal_language_server.path") 7 | local lfs = require("lfs") 8 | local TealProjectConfig = require("teal_language_server.teal_project_config") 9 | local tl = require("tl") 10 | local tracing = require("teal_language_server.tracing") 11 | local class = require("teal_language_server.class") 12 | 13 | local record ServerState 14 | capabilities:{string:any} 15 | name: string 16 | version: string 17 | config: TealProjectConfig 18 | teal_project_root_dir: Path 19 | 20 | _has_initialized: boolean 21 | _teal_project_root_dir: Path 22 | _config: TealProjectConfig 23 | _env: tl.Env 24 | 25 | metamethod __call: function(self: ServerState): ServerState 26 | end 27 | 28 | function ServerState:__init() 29 | self._has_initialized = false 30 | end 31 | 32 | local capabilities = { 33 | -- we basically do the bare minimum 34 | textDocumentSync = { 35 | openClose = true, 36 | change = lsp.sync_kind.Full, 37 | save = { 38 | includeText = true, 39 | }, 40 | }, 41 | hoverProvider = true, 42 | definitionProvider = true, 43 | completionProvider = { 44 | triggerCharacters = { ".", ":" }, 45 | }, 46 | signatureHelpProvider = { 47 | triggerCharacters = { "(" } 48 | } 49 | } 50 | 51 | function ServerState:_validate_config(c:TealProjectConfig) 52 | asserts.that(type(c) == "table", "Expected table, got {}", type(c)) 53 | 54 | local function sort_in_place(t: {Value}, fn?: function(Value, Value): boolean): {Value} 55 | table.sort(t, fn) 56 | return t 57 | end 58 | 59 | local function from(fn: function(...: any): (Value), ...: any): {Value} 60 | local t = {} 61 | for val in fn, ... do 62 | table.insert(t, val) 63 | end 64 | return t 65 | end 66 | 67 | local function keys(t: {Key:any}): function(): Key 68 | local k: Key 69 | return function(): Key 70 | k = next(t, k) 71 | return k 72 | end 73 | end 74 | 75 | local function values(t: {Key:Value}): function(): Value 76 | local k, v: Key, Value 77 | return function(): Value 78 | k, v = next(t, k) 79 | return v 80 | end 81 | end 82 | 83 | local function get_types_in_array(val: {any}, typefn?: function(any): string): {string} 84 | typefn = typefn or type 85 | local set = {} 86 | for _, v in ipairs(val) do 87 | set[typefn(v)] = true 88 | end 89 | return sort_in_place(from(keys(set))) 90 | end 91 | 92 | local function get_array_type(val: any, default: string): string 93 | if type(val) ~= "table" then 94 | return type(val) 95 | end 96 | local ts = get_types_in_array(val as {any}) 97 | if #ts == 0 then 98 | ts[1] = default 99 | end 100 | return "{" .. table.concat(ts, "|") .. "}" 101 | end 102 | 103 | local function get_map_type(val: any, default_key: string, default_value?: string): string 104 | if type(val) ~= "table" then 105 | return type(val) 106 | end 107 | 108 | local key_types = get_types_in_array(from(keys(val as {any:any}))) 109 | if #key_types == 0 then 110 | key_types[1] = default_key 111 | end 112 | 113 | -- bias values towards array types, since we probably won't use nested maps 114 | local val_types = get_types_in_array(from(values(val as {any:any})), get_array_type as function(any): string) 115 | if #val_types == 0 then 116 | val_types[1] = default_value 117 | end 118 | return "{" .. table.concat(key_types, "|") .. ":" .. table.concat(val_types, "|") .. "}" 119 | end 120 | 121 | local valid_keys : {string:string|{string:boolean}} = { 122 | build_dir = "string", 123 | source_dir = "string", 124 | module_name = "string", 125 | 126 | include = "{string}", 127 | exclude = "{string}", 128 | 129 | include_dir = "{string}", 130 | global_env_def = "string", 131 | scripts = "{string:{string}}", 132 | 133 | gen_compat = { ["off"] = true, ["optional"] = true, ["required"] = true }, 134 | gen_target = { ["5.1"] = true, ["5.3"] = true }, 135 | 136 | disable_warnings = "{string}", 137 | warning_error = "{string}", 138 | } 139 | 140 | local errs : {string} = {} 141 | local warnings : {string} = {} 142 | 143 | for k, v in pairs(c as {string:any}) do 144 | if k == "externals" then 145 | if type(v) ~= "table" then 146 | table.insert(errs, "Expected externals to be a table, got " .. type(v)) 147 | end 148 | else 149 | local valid = valid_keys[k] 150 | if not valid then 151 | table.insert(warnings, string.format("Unknown key '%s'", k)) 152 | elseif valid is {string:boolean} then 153 | if not valid[v as string] then 154 | local sorted_keys = sort_in_place(from(keys(valid))) 155 | table.insert(errs, "Invalid value for " .. k .. ", expected one of: " .. table.concat(sorted_keys, ", ")) 156 | end 157 | else 158 | local vtype = valid:find(":") 159 | and get_map_type(v, valid:match("^{(.*):(.*)}$")) 160 | or get_array_type(v, valid:match("^{(.*)}$")) 161 | 162 | if vtype ~= valid then 163 | table.insert(errs, string.format("Expected %s to be a %s, got %s", k, valid, vtype)) 164 | end 165 | end 166 | end 167 | end 168 | 169 | local function verify_non_absolute_path(key: string) 170 | local val = (c as {string:string})[key] 171 | if type(val) ~= "string" then 172 | -- error already generated an error or wasn't provided 173 | return 174 | end 175 | local as_path = Path(val) 176 | if as_path:is_absolute() then 177 | table.insert(errs, string.format("Expected a non-absolute path for %s, got %s", key, as_path.value)) 178 | end 179 | end 180 | verify_non_absolute_path("source_dir") 181 | verify_non_absolute_path("build_dir") 182 | 183 | local function verify_warnings(key: string) 184 | local arr = (c as {string:{string}})[key] 185 | if arr then 186 | for _, warning in ipairs(arr) do 187 | if not tl.warning_kinds[warning as tl.WarningKind] then 188 | table.insert(errs, string.format("Unknown warning in %s: %q", key, warning)) 189 | end 190 | end 191 | end 192 | end 193 | verify_warnings("disable_warnings") 194 | verify_warnings("warning_error") 195 | 196 | asserts.that(#errs == 0, "Found {} errors and {} warnings in config:\n{}\n{}", #errs, #warnings, errs, warnings) 197 | 198 | if #warnings > 0 then 199 | tracing.warning(_module_name, "Found {} warnings in config:\n{}", {#warnings, warnings}) 200 | end 201 | end 202 | 203 | function ServerState:_load_config(root_dir:Path):TealProjectConfig 204 | local config_path = root_dir:join("tlconfig.lua") 205 | if config_path:exists() == false then 206 | return {} as TealProjectConfig 207 | end 208 | 209 | local success, result = pcall(dofile, config_path.value) 210 | 211 | if success then 212 | local config = result as TealProjectConfig 213 | self:_validate_config(config) 214 | return config 215 | end 216 | 217 | asserts.fail("Failed to parse tlconfig: {}", result) 218 | end 219 | 220 | function ServerState:set_env(env:tl.Env) 221 | asserts.is_not_nil(env) 222 | self._env = env 223 | end 224 | 225 | function ServerState:get_env(): tl.Env 226 | asserts.is_not_nil(self._env) 227 | return self._env 228 | end 229 | 230 | function ServerState:initialize(root_dir:Path) 231 | asserts.that(not self._has_initialized) 232 | self._has_initialized = true 233 | 234 | self._teal_project_root_dir = root_dir 235 | asserts.that(lfs.chdir(root_dir.value), "unable to chdir into {}", root_dir.value) 236 | 237 | self._config = self:_load_config(root_dir) 238 | end 239 | 240 | class.setup(ServerState, "ServerState", { 241 | getters = { 242 | capabilities = function():{string:any} 243 | return capabilities 244 | end, 245 | name = function():string 246 | return "teal-language-server" 247 | end, 248 | version = function():string 249 | return "0.0.1" 250 | end, 251 | teal_project_root_dir = function(self:ServerState):Path 252 | return self._teal_project_root_dir 253 | end, 254 | config = function(self:ServerState):TealProjectConfig 255 | return self._config 256 | end, 257 | }, 258 | nilable_members = { 259 | '_teal_project_root_dir', '_config', '_env' 260 | } 261 | }) 262 | 263 | return ServerState 264 | -------------------------------------------------------------------------------- /src/teal_language_server/stdin_reader.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "stdin_reader" 2 | 3 | -- 4 | local lusc = require("lusc") 5 | local asserts = require("teal_language_server.asserts") 6 | local uv = require("luv") 7 | local tracing = require("teal_language_server.tracing") 8 | local class = require("teal_language_server.class") 9 | 10 | local record StdinReader 11 | _stdin: uv.Pipe 12 | _buffer: string 13 | _chunk_added_event: lusc.PulseEvent 14 | _disposed: boolean 15 | 16 | metamethod __call: function(self: StdinReader): StdinReader 17 | end 18 | 19 | function StdinReader:__init() 20 | self._buffer = "" 21 | self._disposed = false 22 | self._chunk_added_event = lusc.new_pulse_event() 23 | end 24 | 25 | function StdinReader:initialize() 26 | self._stdin = uv.new_pipe(false) 27 | asserts.that(self._stdin ~= nil) 28 | assert(self._stdin:open(0)) 29 | tracing.trace(_module_name, "Opened pipe for stdin. Now waiting to receive data...") 30 | 31 | assert(self._stdin:read_start(function(err:string, chunk:string) 32 | if self._disposed then 33 | return 34 | end 35 | assert(not err, err) 36 | if chunk then 37 | tracing.trace(_module_name, "Received new data chunk from stdin: {}", {chunk}) 38 | 39 | self._buffer = self._buffer .. chunk 40 | self._chunk_added_event:set() 41 | end 42 | end)) 43 | end 44 | 45 | function StdinReader:dispose() 46 | asserts.that(not self._disposed) 47 | self._disposed = true 48 | assert(self._stdin:read_stop()) 49 | self._stdin:close() 50 | tracing.debug(_module_name, "Closed pipe for stdin") 51 | end 52 | 53 | function StdinReader:read_line():string 54 | asserts.that(not self._disposed) 55 | tracing.trace(_module_name, "Attempting to read line from stdin...") 56 | asserts.that(lusc.is_available()) 57 | 58 | while true do 59 | local i = self._buffer:find("\n") 60 | 61 | if i then 62 | local line = self._buffer:sub(1, i - 1) 63 | self._buffer = self._buffer:sub(i + 1) 64 | line = line:gsub("\r$", "") 65 | tracing.trace(_module_name, "Successfully parsed line from buffer: {}. Buffer is now: {}", {line, self._buffer}) 66 | return line 67 | else 68 | tracing.trace(_module_name, "No line available yet. Waiting for more data...", {}) 69 | self._chunk_added_event:await() 70 | tracing.trace(_module_name, "Checking stdin again for new line...", {}) 71 | end 72 | end 73 | end 74 | 75 | function StdinReader:read(len:integer):string 76 | asserts.that(not self._disposed) 77 | tracing.trace(_module_name, "Attempting to read {} characters from stdin...", {len}) 78 | 79 | asserts.that(lusc.is_available()) 80 | 81 | while true do 82 | if #self._buffer >= len then 83 | local data = self._buffer:sub(1, len) 84 | self._buffer = self._buffer:sub(#data + 1) 85 | return data 86 | end 87 | 88 | self._chunk_added_event:await() 89 | end 90 | end 91 | 92 | class.setup(StdinReader, "StdinReader", { 93 | nilable_members = { '_stdin' } 94 | }) 95 | 96 | return StdinReader 97 | -------------------------------------------------------------------------------- /src/teal_language_server/teal_project_config.tl: -------------------------------------------------------------------------------- 1 | 2 | local tl = require("tl") 3 | 4 | local record TealProjectConfig 5 | build_dir: string 6 | source_dir: string 7 | include: {string} 8 | exclude: {string} 9 | global_env_def: string 10 | include_dir: {string} 11 | module_name: string 12 | scripts: {string:{string}} 13 | 14 | gen_compat: tl.GenCompat 15 | gen_target: tl.GenTarget 16 | disable_warnings: {tl.WarningKind} 17 | warning_error: {tl.WarningKind} 18 | 19 | -- externals field to allow for external tools to take entries in the config 20 | -- without our type checking complaining 21 | externals: {string:any} 22 | end 23 | 24 | return TealProjectConfig 25 | -------------------------------------------------------------------------------- /src/teal_language_server/trace_entry.tl: -------------------------------------------------------------------------------- 1 | 2 | local record TraceEntry 3 | enum Level 4 | "ERROR" 5 | "WARNING" 6 | "INFO" 7 | "DEBUG" 8 | "TRACE" 9 | end 10 | 11 | record Source 12 | file: string 13 | line: integer 14 | end 15 | 16 | module: string 17 | level: TraceEntry.Level 18 | timestamp: integer -- unix timestamp in seconds 19 | time: number -- seconds since start of application 20 | 21 | message:string 22 | end 23 | 24 | return TraceEntry 25 | -------------------------------------------------------------------------------- /src/teal_language_server/trace_stream.tl: -------------------------------------------------------------------------------- 1 | 2 | local util = require("teal_language_server.util") 3 | local asserts = require("teal_language_server.asserts") 4 | local Path = require("teal_language_server.path") 5 | local TraceEntry = require("teal_language_server.trace_entry") 6 | local json = require("cjson") 7 | local uv = require("luv") 8 | local class = require("teal_language_server.class") 9 | 10 | local record TraceStream 11 | _file_stream:FILE 12 | _has_initialized:boolean 13 | _is_initializing:boolean 14 | _has_disposed:boolean 15 | log_path:Path 16 | _log_path:Path 17 | 18 | metamethod __call: function(self: TraceStream): TraceStream 19 | end 20 | 21 | function TraceStream:__init() 22 | self._has_initialized = false 23 | self._is_initializing = false 24 | self._has_disposed = false 25 | end 26 | 27 | local function _open_write_file(path:string):FILE 28 | local file = io.open(path, "w+") 29 | asserts.is_not_nil(file, "Could not open file '{}'", path) 30 | file:setvbuf("line") 31 | return file 32 | end 33 | 34 | local function _open_write_file_append(path:string):FILE 35 | local file = io.open(path, "a") 36 | asserts.is_not_nil(file, "Could not open file '{}'", path) 37 | file:setvbuf("line") 38 | return file 39 | end 40 | 41 | function TraceStream:_cleanup_old_logs(dir:Path) 42 | if not dir:is_directory() then 43 | dir:create_directory() 44 | end 45 | 46 | local current_time_sec = os.time() 47 | local max_age_sec = 60 * 60 * 24 -- 1 day 48 | 49 | for _, file_path in ipairs(dir:get_sub_files()) do 50 | local stats = assert(uv.fs_stat(file_path.value)) 51 | local mod_time_sec = stats.mtime.sec 52 | 53 | if current_time_sec - mod_time_sec > max_age_sec then 54 | util.try { 55 | action = function():nil 56 | file_path:delete_file() 57 | end, 58 | catch = function():nil 59 | -- ignore if something has the lock for it 60 | end, 61 | } 62 | end 63 | end 64 | end 65 | 66 | function TraceStream:_get_log_dir():Path 67 | local homedir = Path(assert(uv.os_homedir())) 68 | asserts.that(homedir:exists()) 69 | local log_dir = homedir:join(".cache"):join("teal-language-server") 70 | 71 | if not log_dir:is_directory() then 72 | log_dir:create_directory() 73 | end 74 | 75 | return log_dir 76 | end 77 | 78 | function TraceStream:_choose_log_file_path():Path 79 | local log_dir = self:_get_log_dir() 80 | self:_cleanup_old_logs(log_dir) 81 | 82 | local date = os.date("*t") 83 | local pid = uv.os_getpid() 84 | -- Need to use pid since there can be many instances of teal-language-server running at same time 85 | return log_dir:join(string.format("%d-%d-%d_%d.txt", date.year, date.month, date.day, pid)) 86 | end 87 | 88 | function TraceStream:initialize() 89 | asserts.that(not self._is_initializing) 90 | self._is_initializing = true 91 | 92 | asserts.that(not self._has_initialized) 93 | self._has_initialized = true 94 | 95 | asserts.is_nil(self._file_stream) 96 | 97 | self._file_stream = _open_write_file(self.log_path.value) 98 | self._is_initializing = false 99 | end 100 | 101 | function TraceStream:_close_file() 102 | asserts.is_not_nil(self._file_stream) 103 | self._file_stream:close() 104 | end 105 | 106 | function TraceStream:rename_output_file(new_name:string) 107 | if self._file_stream ~= nil then 108 | self:_close_file() 109 | end 110 | 111 | local new_path = self:_get_log_dir():join(new_name .. ".log") 112 | uv.fs_rename(self._log_path.value, new_path.value) 113 | self._log_path = new_path 114 | self._file_stream = _open_write_file_append(self._log_path.value) 115 | end 116 | 117 | function TraceStream:flush() 118 | if self._has_disposed or self._is_initializing then 119 | return 120 | end 121 | 122 | if self._file_stream ~= nil then 123 | asserts.is_not_nil(self._file_stream) 124 | self._file_stream:flush() 125 | end 126 | end 127 | 128 | function TraceStream:dispose() 129 | asserts.that(not self._has_disposed) 130 | asserts.that(not self._is_initializing) 131 | 132 | self._has_disposed = true 133 | 134 | if not self._has_initialized then 135 | return 136 | end 137 | 138 | if self._file_stream ~= nil then 139 | self:_close_file() 140 | end 141 | end 142 | 143 | function TraceStream:log_entry(entry:TraceEntry):nil 144 | if self._has_disposed or self._is_initializing then 145 | return 146 | end 147 | 148 | if not self._has_initialized then 149 | self:initialize() 150 | end 151 | 152 | asserts.is_not_nil(self._file_stream) 153 | 154 | self._file_stream:write(json.encode(entry) .. "\n") 155 | self._file_stream:flush() 156 | end 157 | 158 | class.setup(TraceStream, "TraceStream", { 159 | nilable_members = { "_file_stream", "_log_path" }, 160 | getters = { 161 | log_path = function(self:TraceStream):Path 162 | if self._log_path == nil then 163 | self._log_path = self:_choose_log_file_path() 164 | asserts.is_not_nil(self._log_path) 165 | end 166 | return self._log_path 167 | end, 168 | } 169 | }) 170 | 171 | return TraceStream 172 | -------------------------------------------------------------------------------- /src/teal_language_server/tracing.tl: -------------------------------------------------------------------------------- 1 | 2 | local TraceEntry = require("teal_language_server.trace_entry") 3 | local asserts = require("teal_language_server.asserts") 4 | local tracing_util = require("teal_language_server.tracing_util") 5 | local uv = require("luv") 6 | 7 | local record tracing 8 | end 9 | 10 | local _streams:{function(TraceEntry)} = {} 11 | 12 | local _level_trace = 0 13 | local _level_debug = 1 14 | local _level_info = 2 15 | local _level_warning = 3 16 | local _level_error = 4 17 | 18 | local level_order_from_str :{TraceEntry.Level:integer} = { 19 | ["TRACE"] = _level_trace, 20 | ["DEBUG"] = _level_debug, 21 | ["INFO"] = _level_info, 22 | ["WARNING"] = _level_warning, 23 | ["ERROR"] = _level_error, 24 | } 25 | 26 | local _min_level:TraceEntry.Level = "TRACE" 27 | local _min_level_number:integer = level_order_from_str[_min_level] 28 | 29 | local function get_unix_timestamp():integer 30 | return os.time() 31 | end 32 | 33 | local _load_start_time:number = nil 34 | 35 | local function get_ref_time_seconds():number 36 | return uv.hrtime() / 1e9 37 | end 38 | 39 | local function get_relative_time():number 40 | if _load_start_time == nil then 41 | _load_start_time = get_ref_time_seconds() 42 | asserts.is_not_nil(_load_start_time) 43 | end 44 | 45 | return get_ref_time_seconds() - _load_start_time 46 | end 47 | 48 | function tracing._is_level_enabled(_log_module:string, level:integer):boolean 49 | if _min_level_number > level then 50 | return false 51 | end 52 | 53 | if #_streams == 0 then 54 | return false 55 | end 56 | 57 | return true 58 | end 59 | 60 | function tracing.add_stream(stream:function(TraceEntry)) 61 | asserts.is_not_nil(stream) 62 | table.insert(_streams, stream) 63 | end 64 | 65 | function tracing.get_min_level():TraceEntry.Level 66 | return _min_level 67 | end 68 | 69 | function tracing.set_min_level(level:TraceEntry.Level) 70 | _min_level = level 71 | _min_level_number = level_order_from_str[level] 72 | end 73 | 74 | local function create_entry(module:string, level:TraceEntry.Level, message_template:string, message_args:{any}):TraceEntry 75 | if message_args == nil then 76 | message_args = {} 77 | end 78 | 79 | local formatted_message = tracing_util.custom_format(message_template, message_args) 80 | 81 | return { 82 | timestamp = get_unix_timestamp(), 83 | time = get_relative_time(), 84 | level = level, 85 | module = module, 86 | message = formatted_message, 87 | } 88 | end 89 | 90 | function tracing.log(module:string, level:TraceEntry.Level, message:string, fields?:{any}) 91 | asserts.is_not_nil(message, "Must provide a non nil value for message") 92 | asserts.that(fields == nil or type(fields) == "table", "Invalid value for fields") 93 | 94 | local entry = create_entry(module, level, message, fields) 95 | 96 | asserts.is_not_nil(entry.message) 97 | 98 | for _, stream in ipairs(_streams) do 99 | stream(entry) 100 | end 101 | end 102 | 103 | function tracing.trace(module:string, message:string, fields?:{any}) 104 | if tracing._is_level_enabled(module, _level_trace) then 105 | tracing.log(module, "TRACE", message, fields) 106 | end 107 | end 108 | 109 | function tracing.debug(module:string, message:string, fields?:{any}) 110 | if tracing._is_level_enabled(module, _level_debug) then 111 | tracing.log(module, "DEBUG", message, fields) 112 | end 113 | end 114 | 115 | function tracing.info(module:string, message:string, fields?:{any}) 116 | if tracing._is_level_enabled(module, _level_info) then 117 | tracing.log(module, "INFO", message, fields) 118 | end 119 | end 120 | 121 | function tracing.warning(module:string, message:string, fields?:{any}) 122 | if tracing._is_level_enabled(module, _level_warning) then 123 | tracing.log(module, "WARNING", message, fields) 124 | end 125 | end 126 | 127 | function tracing.error(module:string, message:string, fields?:{any}) 128 | if tracing._is_level_enabled(module, _level_error) then 129 | tracing.log(module, "ERROR", message, fields) 130 | end 131 | end 132 | 133 | return tracing 134 | 135 | -------------------------------------------------------------------------------- /src/teal_language_server/tracing_util.tl: -------------------------------------------------------------------------------- 1 | 2 | local inspect = require("inspect") 3 | 4 | local record tracing_util 5 | end 6 | 7 | -- we use our own custom formatting convention here which is: 8 | -- :l = do not add quotes around value 9 | -- @ = serialize the value to string somehow 10 | -- 0*.0* (eg. 0.00, 00.0) = pad zeros and choose decimal amount 11 | function tracing_util.custom_tostring(value:any, formatting:string):string 12 | local value_type = type(value) 13 | 14 | if formatting == nil or formatting == "" or formatting == "l" then 15 | if value_type == "thread" then 16 | return "" 17 | elseif value_type == "function" then 18 | return "" 19 | elseif value_type == "string" then 20 | if formatting == "l" then 21 | return value as string 22 | else 23 | return "'" .. (value as string) .. "'" 24 | end 25 | else 26 | return tostring(value) 27 | end 28 | end 29 | 30 | if formatting == "@" then 31 | return inspect(value, { indent = "", newline = " " }) 32 | end 33 | 34 | -- Otherwise, we assume the format string is meant for string.format 35 | return string.format(formatting, value) 36 | end 37 | 38 | function tracing_util.custom_format(message:string, message_args:{any}):string 39 | local count = 0 40 | 41 | -- non greedy match for {} pairs 42 | local pattern = "{(.-)}" 43 | 44 | local expanded_message = string.gsub(message, pattern, function(formatting:string):string 45 | count = count + 1 46 | 47 | local field_value = message_args[count] 48 | return tracing_util.custom_tostring(field_value, formatting) 49 | end) 50 | 51 | return expanded_message 52 | end 53 | 54 | return tracing_util 55 | -------------------------------------------------------------------------------- /src/teal_language_server/uri.tl: -------------------------------------------------------------------------------- 1 | 2 | local asserts = require("teal_language_server.asserts") 3 | local util = require("teal_language_server.util") 4 | 5 | --[[ 6 | uri spec: 7 | 8 | foo://example.com:8042/over/there?name=ferret#nose 9 | \_/ \______________/\_________/ \_________/ \__/ 10 | | | | | | 11 | scheme authority path query fragment 12 | | _____________________|__ 13 | / \ / \ 14 | urn:example:animal:ferret:nose 15 | 16 | see http://tools.ietf.org/html/rfc3986 17 | ]] 18 | 19 | local record Uri 20 | scheme: string 21 | authority: string 22 | path: string 23 | query: string 24 | fragment: string 25 | end 26 | 27 | local function find_patt(str: string, patt: string, pos: integer): string, integer, integer 28 | pos = pos or 1 29 | local s, e = str:find(patt, pos) 30 | if s then 31 | return str:sub(s, e), s, e 32 | end 33 | end 34 | 35 | local function get_next_key_and_patt(current: string): string, string 36 | if current == "://" then 37 | return "authority", "/" 38 | elseif current == "/" then 39 | return "path", "[?#]" 40 | elseif current == "?" then 41 | return "query", "#" 42 | elseif current == "#" then 43 | return "fragment", "$" 44 | end 45 | end 46 | 47 | function Uri.parse(text: string): Uri 48 | if not text then 49 | return nil 50 | end 51 | 52 | -- This is not the most robust uri parser, but we can assume that clients are well behaved 53 | -- and for the most part we only care about the path 54 | local parsed : {string:string} = {} 55 | local last = 1 56 | local next_key = "scheme" 57 | local next_patt = "://" 58 | 59 | while next_patt do 60 | local char , s , e = find_patt(text, next_patt, last) 61 | parsed[next_key] = text:sub(last, (s or 0) - 1) 62 | 63 | next_key, next_patt = get_next_key_and_patt(char) 64 | last = (e or last) 65 | + (next_key == "path" and 0 or 1) 66 | end 67 | 68 | for k, v in pairs(parsed) do 69 | if #v == 0 then 70 | parsed[k] = nil 71 | end 72 | end 73 | 74 | -- if authority is present, path can be empty 75 | if parsed.authority and not parsed.path then 76 | parsed.path = "" 77 | end 78 | 79 | -- Hack for windows paths 80 | -- TODO - reconsider above logic 81 | if util.get_platform() == "windows" and util.string_starts_with(parsed.path, "/") then 82 | parsed.path = parsed.path:sub(2) 83 | end 84 | 85 | -- scheme and path are required 86 | if not (parsed.scheme and parsed.path) then 87 | return nil 88 | end 89 | 90 | -- if authority is not present, path may not begin with '//' 91 | if not parsed.authority and parsed.path:sub(1, 2) == "//" then 92 | return nil 93 | end 94 | 95 | return parsed as Uri 96 | end 97 | 98 | function Uri.path_from_uri(s: string): string 99 | local parsed = Uri.parse(s) 100 | asserts.that(parsed.scheme == "file", "uri " .. tostring(s) .. " is not a file") 101 | return parsed.path 102 | end 103 | 104 | function Uri.uri_from_path(path: string): Uri 105 | return { 106 | scheme = "file", 107 | authority = "", 108 | path = path, 109 | query = nil, 110 | fragment = nil 111 | } 112 | end 113 | 114 | function Uri.tostring(u: Uri): string 115 | return u.scheme .. "://" 116 | .. (u.authority or "") 117 | .. (u.path or "") 118 | .. (u.query and "?" .. u.query or "") 119 | .. (u.fragment and "#" .. u.fragment or "") 120 | end 121 | 122 | return Uri 123 | -------------------------------------------------------------------------------- /src/teal_language_server/util.tl: -------------------------------------------------------------------------------- 1 | local _module_name = "util" 2 | 3 | local uv = require("luv") 4 | local tracing = require("teal_language_server.tracing") 5 | 6 | local record util 7 | record TryOpts 8 | action:function():T 9 | catch:function(err:any):T 10 | finally:function() 11 | end 12 | 13 | enum PlatformType 14 | "linux" 15 | "osx" 16 | "windows" 17 | "unknown" 18 | end 19 | end 20 | 21 | local _uname_info:uv.UnameInfo = nil 22 | local _os_type:util.PlatformType = nil 23 | 24 | local function _get_uname_info():uv.UnameInfo 25 | if _uname_info == nil then 26 | _uname_info = uv.os_uname() 27 | assert(_uname_info ~= nil) 28 | end 29 | 30 | return _uname_info 31 | end 32 | 33 | local function _on_error(error_obj:string):any 34 | return debug.traceback(error_obj, 2) 35 | end 36 | 37 | function util.string_escape_special_chars(value:string):string 38 | -- gsub is not ideal in cases where we want to do a literal 39 | -- replace, so to do this just escape all special characters with '%' 40 | value = value:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0") 41 | -- Note that we don't put this all in the return statement to avoid 42 | -- forwarding the multiple return values causing subtle errors 43 | return value 44 | end 45 | 46 | function util.string_starts_with(str:string, prefix:string):boolean 47 | return str:sub(1, #prefix) == prefix 48 | end 49 | 50 | function util.string_split(str:string, delimiter:string):{string} 51 | -- Unclear whether this should return {} or {''} so just fail instead 52 | -- Most split functions do one of these two things so there isn't really a standard here 53 | -- So force calling code to decide 54 | assert(#str > 0, "Unclear how to split an empty string") 55 | 56 | assert(delimiter ~= nil, "missing delimiter") 57 | assert(delimiter is string) 58 | assert(#delimiter > 0) 59 | 60 | local num_delimiter_chars = #delimiter 61 | 62 | delimiter = util.string_escape_special_chars(delimiter) 63 | 64 | local start_index = 1 65 | local result:{string} = {} 66 | 67 | while true do 68 | local delimiter_index, _ = str:find(delimiter, start_index) 69 | 70 | if delimiter_index == nil then 71 | table.insert(result, str:sub(start_index)) 72 | break 73 | end 74 | 75 | table.insert(result, str:sub(start_index, delimiter_index-1)) 76 | 77 | start_index = delimiter_index + num_delimiter_chars 78 | end 79 | 80 | return result 81 | end 82 | 83 | function util.string_join(delimiter:string, items:{string}):string 84 | assert(delimiter is string) 85 | assert(items ~= nil) 86 | 87 | local result = '' 88 | for _, item in ipairs(items) do 89 | if #result ~= 0 then 90 | result = result .. delimiter 91 | end 92 | result = result .. tostring(item) 93 | end 94 | return result 95 | end 96 | 97 | function util.get_platform():util.PlatformType 98 | if _os_type == nil then 99 | local raw_os_name = string.lower(_get_uname_info().sysname) 100 | 101 | if raw_os_name == "linux" then 102 | _os_type = "linux" 103 | elseif raw_os_name:find("darwin") ~= nil then 104 | _os_type = "osx" 105 | elseif raw_os_name:find("windows") ~= nil or raw_os_name:find("mingw") ~= nil then 106 | _os_type = "windows" 107 | else 108 | tracing.warning(_module_name, "Unrecognized platform {}", {raw_os_name}) 109 | _os_type = "unknown" 110 | end 111 | end 112 | 113 | return _os_type 114 | end 115 | 116 | function util.try(t:util.TryOpts):T 117 | local success, ret_value = xpcall(t.action, _on_error) 118 | if success then 119 | if t.finally then 120 | t.finally() 121 | end 122 | return ret_value 123 | end 124 | if not t.catch then 125 | if t.finally then 126 | t.finally() 127 | end 128 | error(ret_value, 2) 129 | end 130 | success, ret_value = xpcall((function():T 131 | return t.catch(ret_value) 132 | end), _on_error) as (boolean, T) 133 | if t.finally then 134 | t.finally() 135 | end 136 | if success then 137 | return ret_value 138 | end 139 | return error(ret_value, 2) 140 | end 141 | 142 | return util 143 | -------------------------------------------------------------------------------- /teal-language-server-0.1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | rockspec_format = "3.0" 2 | 3 | package = "teal-language-server" 4 | version = "0.1.1-1" 5 | 6 | source = { 7 | url = "git+https://github.com/teal-language/teal-language-server.git", 8 | tag = "0.1.1" 9 | } 10 | 11 | description = { 12 | summary = "A language server for the Teal language", 13 | detailed = "A language server for the Teal language", 14 | homepage = "https://github.com/teal-language/teal-language-server", 15 | license = "MIT" 16 | } 17 | 18 | build_dependencies = { 19 | "luarocks-build-treesitter-parser >= 6.0.0", -- can be removed when tree-sitter-teal specifies this version >= 6 20 | } 21 | 22 | dependencies = { 23 | "luafilesystem", 24 | "tl == 0.24.4", 25 | "lua-cjson", 26 | "argparse", 27 | "inspect", 28 | "luv ~> 1", 29 | "lusc_luv >= 4.0", 30 | "ltreesitter-ts == 0.0.1", -- can be removed when ltreesitter updates 31 | "tree-sitter-cli == 0.24.4", 32 | "tree-sitter-teal == 0.0.33", 33 | } 34 | 35 | test_dependencies = { "busted~>2" } 36 | 37 | test = { 38 | type = "busted", 39 | flags = {"-m", "gen/?.lua"}, 40 | } 41 | 42 | build = { 43 | type = "builtin", 44 | modules = { 45 | ["teal_language_server.args_parser"] = "gen/teal_language_server/args_parser.lua", 46 | ["teal_language_server.asserts"] = "gen/teal_language_server/asserts.lua", 47 | ["teal_language_server.class"] = "gen/teal_language_server/class.lua", 48 | ["teal_language_server.constants"] = "gen/teal_language_server/constants.lua", 49 | ["teal_language_server.document"] = "gen/teal_language_server/document.lua", 50 | ["teal_language_server.document_manager"] = "gen/teal_language_server/document_manager.lua", 51 | ["teal_language_server.env_updater"] = "gen/teal_language_server/env_updater.lua", 52 | ["teal_language_server.lsp"] = "gen/teal_language_server/lsp.lua", 53 | ["teal_language_server.lsp_events_manager"] = "gen/teal_language_server/lsp_events_manager.lua", 54 | ["teal_language_server.lsp_formatter"] = "gen/teal_language_server/lsp_formatter.lua", 55 | ["teal_language_server.lsp_reader_writer"] = "gen/teal_language_server/lsp_reader_writer.lua", 56 | ["teal_language_server.main"] = "gen/teal_language_server/main.lua", 57 | ["teal_language_server.misc_handlers"] = "gen/teal_language_server/misc_handlers.lua", 58 | ["teal_language_server.path"] = "gen/teal_language_server/path.lua", 59 | ["teal_language_server.server_state"] = "gen/teal_language_server/server_state.lua", 60 | ["teal_language_server.stdin_reader"] = "gen/teal_language_server/stdin_reader.lua", 61 | ["teal_language_server.teal_project_config"] = "gen/teal_language_server/teal_project_config.lua", 62 | ["teal_language_server.trace_entry"] = "gen/teal_language_server/trace_entry.lua", 63 | ["teal_language_server.trace_stream"] = "gen/teal_language_server/trace_stream.lua", 64 | ["teal_language_server.tracing"] = "gen/teal_language_server/tracing.lua", 65 | ["teal_language_server.tracing_util"] = "gen/teal_language_server/tracing_util.lua", 66 | ["teal_language_server.uri"] = "gen/teal_language_server/uri.lua", 67 | ["teal_language_server.util"] = "gen/teal_language_server/util.lua", 68 | }, 69 | install = { 70 | bin = { 71 | ['teal-language-server'] = 'bin/teal-language-server' 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tlconfig.lua: -------------------------------------------------------------------------------- 1 | return { 2 | build_dir = "gen", 3 | source_dir = "src", 4 | include_dir = { "src", "types" }, 5 | } 6 | -------------------------------------------------------------------------------- /types/argparse.d.tl: -------------------------------------------------------------------------------- 1 | 2 | local record argparse 3 | type Args = {string : {string} | string | boolean} 4 | 5 | record Parser 6 | name: function(self: Parser, name: string): Parser 7 | description: function(self: Parser, description: string): Parser 8 | epilog: function(self: Parser, epilog: string): Parser 9 | 10 | flag: function(self: Parser, flag: string): Option 11 | flag: function(self: Parser, shortalias: string, longalias: string): Option 12 | 13 | parse: function(self: Parser, argv?: {string}): Args 14 | pparse: function(self: Parser, argv?: {string}): boolean, Args | string 15 | 16 | error: function(self: Parser, error: string) 17 | 18 | argument: function(self: Parser, name: string, description: string): Argument 19 | 20 | get_usage: function(self: Parser): string 21 | get_help: function(self: Parser): string 22 | 23 | option: function(self: Parser, name: string, description?: string, default?: string, convert?: function | {function}, args?: {string}, count: number | string): Option 24 | option: function(self: Parser, name: string, description?: string, default?: string, convert?: {string:string}, args?: {string}, count: number | string): Option 25 | 26 | require_command: function(self: Parser, require_command: boolean): Parser 27 | command_target: function(self: Parser, command_target: string): Parser 28 | 29 | command: function(self: Parser, name: string, description: string, epilog: string): Command 30 | 31 | add_help: function(self: Parser, boolean) 32 | 33 | -- TODO: should be Argument | Option 34 | mutex: function(self: Parser, ...: any) 35 | 36 | record Opts 37 | name: string 38 | description: string 39 | epilog: string 40 | end 41 | metamethod __call: function(Parser, Opts): Parser 42 | metamethod __call: function(Parser, string, string, string): Parser 43 | end 44 | 45 | type ActionCallback = function(args: Args, index: string, val: string | boolean | {string}, overwrite: boolean) 46 | 47 | record Argument 48 | choices: function(self: Argument, choices: {string}): Argument 49 | 50 | convert: function(self: Argument, convert: function | {function}): Argument 51 | convert: function(self: Argument, convert: {string:string}): Argument 52 | 53 | args: function(self: Argument, args: string | number): Argument 54 | 55 | action: function(self: Argument, cb: ActionCallback) 56 | end 57 | 58 | record Option 59 | name: function(self: Option, name: string): Option 60 | description: function(self: Option, description: string): Option 61 | 62 | argname: function(self: Option, argname: string | {string}): Option 63 | 64 | count: function(self: Option, count: number | string): Option 65 | 66 | choices: function(self: Option, {string}): Option 67 | 68 | default: function(self: Option, string): Option 69 | 70 | defmode: function(self: Option, string): Option 71 | 72 | target: function(self: Option, target: string): Option 73 | 74 | args: function(self: Option, args: string|number): Option 75 | 76 | action: function(self: Option, cb: ActionCallback) 77 | end 78 | 79 | record Command 80 | summary: function(self: Command, summary: string): Command 81 | description: function(self: Command, description: string): Command 82 | 83 | argument: function(self: Command, name: string, description: string): Argument 84 | 85 | option: function(self: Command, name: string, description?: string): Option 86 | 87 | flag: function(self: Command, string, string): Option 88 | end 89 | 90 | metamethod __call: function(self: argparse, name: string, description?: string, epilog?: string): Parser 91 | end 92 | 93 | return argparse 94 | -------------------------------------------------------------------------------- /types/cjson.d.tl: -------------------------------------------------------------------------------- 1 | 2 | local record cjson 3 | record Null 4 | userdata 5 | end 6 | encode: function({string:any}): string 7 | decode: function(string): {string:any} 8 | null: Null 9 | end 10 | 11 | return cjson 12 | -------------------------------------------------------------------------------- /types/inspect.d.tl: -------------------------------------------------------------------------------- 1 | local record inspect 2 | record InspectOptions 3 | depth: number 4 | newline: string 5 | indent: string 6 | process: function(item: any, path: {any}): any 7 | end 8 | 9 | metamethod __call: function(self: inspect, value: any, options?: InspectOptions): string 10 | end 11 | 12 | return inspect 13 | -------------------------------------------------------------------------------- /types/lfs.d.tl: -------------------------------------------------------------------------------- 1 | local record lfs 2 | 3 | enum FileMode 4 | "file" 5 | "directory" 6 | "link" 7 | "socket" 8 | "named pipe" 9 | "char device" 10 | "block device" 11 | "other" 12 | end 13 | 14 | record Attributes 15 | dev: number 16 | ino: number 17 | mode: FileMode 18 | nlink: number 19 | uid: number 20 | gid: number 21 | rdev: number 22 | access: number 23 | modification: number 24 | change: number 25 | size: number 26 | permissions: string 27 | blocks: number 28 | blksize: number 29 | end 30 | 31 | enum OpenFileMode 32 | "binary" 33 | "text" 34 | end 35 | 36 | enum LockMode 37 | "r" 38 | "w" 39 | end 40 | 41 | record Lock 42 | free: function() 43 | end 44 | 45 | dir: function(string): function(): string 46 | 47 | chdir: function(string): boolean, string 48 | 49 | lock_dir: function(string, number): Lock, string 50 | 51 | -- returns number on success, really!? this should be fixed in the lfs library 52 | link: function(string, string, boolean): number, string 53 | 54 | mkdir: function(string): boolean, string 55 | 56 | rmdir: function(string): boolean, string 57 | 58 | setmode: function(string, OpenFileMode): boolean, string 59 | 60 | currentdir: function(): string 61 | 62 | attributes: function(string): Attributes 63 | attributes: function(string, string): string 64 | attributes: function(string, string): number 65 | attributes: function(string, string): FileMode 66 | attributes: function(string, Attributes): Attributes 67 | 68 | symlinkattributes: function(string): Attributes 69 | symlinkattributes: function(string, string): string 70 | symlinkattributes: function(string, string): number 71 | symlinkattributes: function(string, string): FileMode 72 | symlinkattributes: function(string, Attributes): Attributes 73 | 74 | touch: function(string, number, number): boolean, string 75 | 76 | -- TODO: FILE needs to be renamed to io.FILE in tl itself 77 | lock: function(FILE, LockMode, number, number): boolean, string 78 | unlock: function(FILE, number, number): boolean, string 79 | 80 | end 81 | 82 | return lfs 83 | -------------------------------------------------------------------------------- /types/ltreesitter.d.tl: -------------------------------------------------------------------------------- 1 | -- Autogenerated type definitions 2 | 3 | local record ltreesitter 4 | -- Exports 5 | 6 | record Cursor userdata 7 | current_field_name: function(Cursor): string -- csrc/tree_cursor.c:32 8 | current_node: function(Cursor): Node -- csrc/tree_cursor.c:20 9 | goto_first_child: function(Cursor): boolean -- csrc/tree_cursor.c:74 10 | goto_first_child_for_byte: function(Cursor, integer): integer -- csrc/tree_cursor.c:83 11 | goto_next_sibling: function(Cursor): boolean -- csrc/tree_cursor.c:65 12 | goto_parent: function(Cursor): boolean -- csrc/tree_cursor.c:56 13 | reset: function(Cursor, Node) -- csrc/tree_cursor.c:46 14 | end 15 | record Node userdata 16 | child: function(Node, idx: integer): Node -- csrc/node.c:129 17 | child_by_field_name: function(Node, string): Node -- csrc/node.c:339 18 | child_count: function(Node): integer -- csrc/node.c:146 19 | children: function(Node): function(): Node -- csrc/node.c:218 20 | create_cursor: function(Node): Cursor -- csrc/node.c:372 21 | end_byte: function(Node): integer -- csrc/node.c:39 22 | end_point: function(Node): Point -- csrc/node.c:72 23 | is_extra: function(Node): boolean -- csrc/node.c:106 24 | is_missing: function(Node): boolean -- csrc/node.c:97 25 | is_named: function(Node): boolean -- csrc/node.c:88 26 | name: function(Node): string -- csrc/node.c:319 27 | named_child: function(Node, idx: integer): Node -- csrc/node.c:155 28 | named_child_count: function(Node): integer -- csrc/node.c:170 29 | named_children: function(Node): function(): Node -- csrc/node.c:232 30 | next_named_sibling: function(Node): Node -- csrc/node.c:274 31 | next_sibling: function(Node): Node -- csrc/node.c:244 32 | prev_named_sibling: function(Node): Node -- csrc/node.c:289 33 | prev_sibling: function(Node): Node -- csrc/node.c:259 34 | source: function(Node): string -- csrc/node.c:357 35 | start_byte: function(Node): integer -- csrc/node.c:30 36 | start_point: function(Node): Point -- csrc/node.c:55 37 | type: function(Node): string -- csrc/node.c:21 38 | end 39 | record Parser userdata 40 | get_ranges: function(Parser): {Range} -- csrc/parser.c:633 41 | get_version: function(Parser): integer -- csrc/parser.c:682 42 | parse_string: function(Parser, string, ? Tree): Tree -- csrc/parser.c:377 43 | parse_with: function(Parser, reader: function(integer, Point): (string), old_tree: Tree): Tree -- csrc/parser.c:470 44 | query: function(Parser, string): Query -- csrc/parser.c:650 45 | set_ranges: function(Parser, {Range}): boolean -- csrc/parser.c:554 46 | set_timeout: function(Parser, integer) -- csrc/parser.c:520 47 | end 48 | record Query userdata 49 | capture: function(Query, Node, start: integer | Point, end_: integer | Point): function(): (Node, string) -- csrc/query.c:514 50 | exec: function(Query, Node, start: integer | Point, end_: integer | Point) -- csrc/query.c:602 51 | match: function(Query, Node, start: integer | Point, end_: integer | Point): function(): Match -- csrc/query.c:468 52 | source: function(Query): string -- csrc/query.c:657 53 | with: function(Query, {string:function(...: string | Node | {Node}): any...}): Query -- csrc/query.c:543 54 | end 55 | record Tree userdata 56 | copy: function(Tree): Tree -- csrc/tree.c:90 57 | edit: function( 58 | Tree, 59 | start_byte: integer, 60 | old_end_byte: integer, 61 | new_end_byte: integer, 62 | start_point_row: integer, 63 | start_point_col: integer, 64 | old_end_point_row: integer, 65 | old_end_point_col: integer, 66 | new_end_point_row: integer, 67 | new_end_point_col: integer 68 | ) -- csrc/tree.c:177 69 | edit_s: function(Tree, TreeEdit) -- csrc/tree.c:120 70 | get_changed_ranges: function(old: Tree, new: Tree): {Range} -- csrc/tree.c:206 71 | root: function(Tree): Node -- csrc/tree.c:71 72 | end 73 | load: function(file_name: string, language_name: string): Parser, string -- csrc/parser.c:111 74 | require: function(library_file_name: string, language_name: string): Parser -- csrc/parser.c:167 75 | version: string -- csrc/ltreesitter.c:14 76 | 77 | -- Inlines 78 | 79 | record TreeEdit 80 | start_byte: integer 81 | old_end_byte: integer 82 | new_end_byte: integer 83 | 84 | start_point: Point 85 | old_end_point: Point 86 | new_end_point: Point 87 | end -- csrc/tree.c:109 88 | 89 | record Match 90 | id: integer 91 | pattern_index: integer 92 | capture_count: integer 93 | captures: {string:Node|{Node}} 94 | end -- csrc/query.c:328 95 | 96 | record Point 97 | row: integer 98 | column: integer 99 | end -- csrc/node.c:48 100 | 101 | record Range 102 | start_byte: integer 103 | end_byte: integer 104 | 105 | start_point: Point 106 | end_point: Point 107 | end -- csrc/parser.c:531 108 | end 109 | 110 | return ltreesitter 111 | 112 | -------------------------------------------------------------------------------- /types/lusc.d.tl: -------------------------------------------------------------------------------- 1 | 2 | local record lusc 3 | record Scheduler 4 | schedule:function(Scheduler, delay_seconds:number, callback:function()) 5 | dispose:function(Scheduler) 6 | end 7 | 8 | record DefaultScheduler 9 | schedule:function(DefaultScheduler, delay_seconds:number, callback:function()) 10 | dispose:function(DefaultScheduler) 11 | 12 | new:function():DefaultScheduler 13 | end 14 | 15 | record Channel 16 | --- Only needed when there is a buffer max size 17 | -- @return true if the receiving side is closed, in which 18 | -- case there is no need to send any more values 19 | await_send:function(Channel, value:T) 20 | 21 | --- raises an error if the buffer is full 22 | -- @return true if the receiving side is closed, in which 23 | -- case there is no need to send any more values 24 | send:function(Channel, value:T) 25 | 26 | --- @return true if both the sending side is closed and there are no more 27 | -- @return received value 28 | -- values to receive 29 | await_receive_next:function(Channel):T, boolean 30 | 31 | as_iterator:function(Channel):(function():T) 32 | 33 | --- Receives all values, until sender is closed 34 | await_receive_all:function(Channel):function():T 35 | 36 | --- raises an error if nothing is there to receive 37 | -- @return received value 38 | -- @return true if both the sending side is closed and there are no more 39 | -- values to receive 40 | receive_next:function(Channel):T, boolean 41 | 42 | --- Indicates that the sender has completed and receiver can end 43 | close:function(Channel) 44 | 45 | -- Just calls close() after the given function completes 46 | close_after:function(Channel, function()) 47 | 48 | is_closed: function(Channel):boolean 49 | end 50 | 51 | record Opts 52 | -- Default: false 53 | generate_debug_names:boolean 54 | 55 | -- err is nil when completed successfully 56 | on_completed: function(err:ErrorGroup) 57 | 58 | -- Optional - by default it uses luv timer 59 | scheduler_factory: function():Scheduler 60 | end 61 | 62 | record ErrorGroup 63 | errors:{any} 64 | new:function({any}):ErrorGroup 65 | end 66 | 67 | record Task 68 | record Opts 69 | name:string 70 | end 71 | 72 | parent: Task 73 | total_active_time: number 74 | end 75 | 76 | record StickyEvent 77 | is_set:boolean 78 | 79 | unset:function(StickyEvent) 80 | set:function(StickyEvent) 81 | await:function(StickyEvent) 82 | end 83 | 84 | record PulseEvent 85 | set:function(PulseEvent) 86 | await:function(PulseEvent) 87 | end 88 | 89 | record CancelledError 90 | end 91 | 92 | record DeadlineOpts 93 | -- note: can only set one of these 94 | move_on_after:number 95 | move_on_at:number 96 | fail_after:number 97 | fail_at:number 98 | end 99 | 100 | record CancelScope 101 | record Opts 102 | shielded: boolean 103 | name:string 104 | 105 | -- note: can only set one of these 106 | move_on_after:number 107 | move_on_at:number 108 | fail_after:number 109 | fail_at:number 110 | end 111 | 112 | record ShortcutOpts 113 | shielded: boolean 114 | name:string 115 | end 116 | 117 | record Result 118 | was_cancelled: boolean 119 | hit_deadline: boolean 120 | end 121 | 122 | has_cancelled:function(CancelScope):boolean 123 | 124 | cancel:function(CancelScope) 125 | end 126 | 127 | record Nursery 128 | record Opts 129 | name:string 130 | 131 | shielded: boolean 132 | 133 | -- note: can only set one of these 134 | move_on_after:number 135 | move_on_at:number 136 | fail_after:number 137 | fail_at:number 138 | end 139 | 140 | cancel_scope: CancelScope 141 | 142 | -- TODO 143 | -- start:function() 144 | 145 | start_soon:function(self: Nursery, func:function(), ?Task.Opts) 146 | end 147 | 148 | open_nursery:function(handler:function(nursery:Nursery), opts:Nursery.Opts):CancelScope.Result 149 | get_time:function():number 150 | await_sleep:function(seconds:number) 151 | await_until:function(until_time:number) 152 | await_forever:function() 153 | new_sticky_event:function():StickyEvent 154 | new_pulse_event:function():PulseEvent 155 | start:function(opts:Opts) 156 | 157 | -- Note that this will only cancel tasks if one of the move_on* or fail_* options 158 | -- are provided. Otherwise it will wait forever for tasks to complete gracefully 159 | -- Note also that if block_until_stopped is provided, it will block 160 | stop:function(opts?:DeadlineOpts) 161 | 162 | -- Long running tasks can check this periodically, and then shut down 163 | -- gracefully, instead of relying on cancels 164 | -- Can also observe via the subscribe_ methods below 165 | stop_requested:function():boolean 166 | 167 | subscribe_stop_requested:function(observer:function()) 168 | unsubscribe_stop_requested:function(observer:function()) 169 | 170 | -- If true, then the current code is being executed 171 | -- under the lusc task loop and therefore lusc await 172 | -- methods can be used 173 | is_available:function():boolean 174 | 175 | try_get_async_local:function(key:any):any 176 | set_async_local:function(key:any, value:any) 177 | 178 | move_on_after:function(delay_seconds:number, handler:function(scope:CancelScope), opts:CancelScope.ShortcutOpts):CancelScope.Result 179 | move_on_at:function(delay_seconds:number, handler:function(scope:CancelScope), opts:CancelScope.ShortcutOpts):CancelScope.Result 180 | fail_after:function(delay_seconds:number, handler:function(scope:CancelScope), opts:CancelScope.ShortcutOpts):CancelScope.Result 181 | fail_at:function(delay_seconds:number, handler:function(scope:CancelScope), opts:CancelScope.ShortcutOpts):CancelScope.Result 182 | 183 | cancel_scope:function(handler:function(scope:CancelScope), opts:CancelScope.Opts):CancelScope.Result 184 | 185 | --- @return true if the given object is an instance of ErrorGroup 186 | -- and also that it only consists of the cancelled error 187 | is_cancelled_error:function(err:any):boolean 188 | 189 | schedule:function(handler:function(), opts?:Task.Opts) 190 | 191 | schedule_wrap: function(function(), opts:Task.Opts): function() 192 | schedule_wrap: function(function(T), opts:Task.Opts): function(T) 193 | schedule_wrap: function(function(T1, T2), opts:Task.Opts): function(T1, T2) 194 | 195 | has_started:function():boolean 196 | 197 | get_root_nursery:function():Nursery 198 | 199 | cancel_all:function() 200 | open_channel:function(max_buffer_size:integer):Channel 201 | 202 | get_running_task:function():Task 203 | try_get_running_task:function():Task 204 | 205 | force_unavailable:function(handler:function():T):T 206 | force_unavailable_wrap:function(handler:function()):function() 207 | 208 | set_log_handler:function(function(message:string)) 209 | 210 | set_impl:function(any) 211 | end 212 | 213 | return lusc 214 | 215 | --------------------------------------------------------------------------------