├── .gitignore ├── .luacheckrc ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── bin └── tarantool-lsp ├── editors.md ├── gen_completion.sh ├── images ├── completion.gif └── hover.gif ├── readme.md ├── scripts ├── compile_data.lua ├── install_hooks.sh ├── observe.lua ├── pre-commit.sh ├── pre-push.sh └── watch_busted.fish ├── spec ├── complete_spec.lua ├── definition_spec.lua ├── format_spec.lua ├── hover_spec.lua ├── log_spec.lua ├── luacheck_spec.lua ├── mock_loop.lua ├── parser_spec.lua ├── symbols_spec.lua ├── test-scm-0.rockspec └── unicode_spec.lua ├── tarantool-lsp-scm-1.rockspec ├── tarantool-lsp.lua ├── tarantool-lsp ├── analyze.lua ├── current-tag-doc ├── data │ └── 5_1.lua ├── formatting.lua ├── inspect.lua ├── io-loop.lua ├── log.lua ├── loop.lua ├── lua-parser │ ├── parser.lua │ ├── scope.lua │ └── validator.lua ├── methods.lua ├── rpc.lua ├── tnt-doc │ ├── completion-generator.lua │ ├── doc-manager.lua │ └── doc-parser.lua ├── unicode.lua ├── utils.lua ├── websocket-lib.lua └── websocket.lua ├── tarantoollsp.rb └── test └── completions ├── golden_files ├── csv.lua └── json.lua └── libs_completion_test.lua /.gitignore: -------------------------------------------------------------------------------- 1 | test.lua 2 | .luacheckcache 3 | 4 | # Tarantool-specific files 5 | **/**.snap 6 | **/**.xlog 7 | /.rocks 8 | 9 | # Logging 10 | log.txt 11 | **/**.log 12 | work_log.md 13 | 14 | # MacOS 15 | .DS_Store 16 | 17 | # Completions 18 | /completions/ 19 | tarantool-lsp/completions/ 20 | .tnt_doc 21 | 22 | 23 | # idea 24 | .idea 25 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { 2 | "Args", 3 | "Config", 4 | "Documents", 5 | "Globals", 6 | "Initialized", 7 | "Root", 8 | "Shutdown", 9 | "Types", 10 | } 11 | std="max" 12 | 13 | files["spec/*.lua"].std = "+busted" 14 | files["spec/*.rockspec"].std = "rockspec" 15 | 16 | files["rockspecs/*.rockspec"].std = "rockspec" 17 | files["*.rockspec"].std = "rockspec" 18 | 19 | include_files = {"lua-lsp", "spec", "*.rockspec"} 20 | exclude_files = {"lua-lsp/data"} 21 | 22 | cache = true 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 0.0.2 6 | 7 | - Fix ws shutdown 8 | - Fix ws method with no return answer 9 | 10 | ## 0.0.1 11 | 12 | Initial release 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kyle McLamb 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | tarantool-lsp/completions: tarantool-lsp/current-tag-doc 3 | ./gen_completion.sh 4 | 5 | all: tarantool-lsp/completions 6 | 7 | install: 8 | mkdir -p $(INST_LUADIR)/tarantool-lsp/completions 9 | cp -r tarantool-lsp/completions/* $(INST_LUADIR)/tarantool-lsp/completions/ 10 | -------------------------------------------------------------------------------- /bin/tarantool-lsp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local fio = require('fio') 4 | local log = require('log') 5 | local fun = require('fun') 6 | local argparse = require('internal.argparse').parse 7 | 8 | local start_dir = fio.dirname(arg[0]) 9 | local link_target_path = fio.readlink(arg[0]) 10 | local lsp_bin 11 | 12 | if link_target_path then 13 | -- File link may point on relative or absolute path 14 | if link_target_path:startswith('.') then 15 | lsp_bin = fio.pathjoin(start_dir, link_target_path) 16 | else 17 | lsp_bin = link_target_paths 18 | end 19 | else 20 | lsp_bin = fio.abspath(arg[0]) 21 | end 22 | 23 | local lsp_root = fio.dirname(fio.dirname(lsp_bin)) 24 | package.setsearchroot(lsp_root) 25 | 26 | _G._ROOT_PATH = lsp_root 27 | 28 | -- For debug purposes 29 | --box.cfg{ 30 | -- log = "log.txt", 31 | -- log_level = 6 32 | --} 33 | 34 | local self_name = fio.basename(arg[0]) 35 | local command_name = arg[1] 36 | 37 | local positional_arguments 38 | local keyword_arguments 39 | 40 | local function exit_wrapper(func) 41 | return function() os.exit(func()) end 42 | end 43 | 44 | local function docs_info(args) 45 | if not fio.path.exists(fio.pathjoin(lsp_root, '.tnt_doc')) then 46 | log.error([[ 47 | Documentation not yet inited! 48 | Call for the initializing. ]]) 49 | return 0 50 | end 51 | 52 | -- TODO: Extend functionality 53 | log.error("Docs was inited") 54 | return 0 55 | end 56 | 57 | local function docs_init() 58 | local doc_manager = require('tarantool-lsp.tnt-doc.doc-manager') 59 | local ok, err = doc_manager.initDoc({ 60 | doc_dir = fio.pathjoin(lsp_root, '.tnt_doc'), 61 | completion_dir = fio.pathjoin(lsp_root, 'completions') 62 | }) 63 | if not ok then 64 | log.error(err) 65 | return 1 66 | end 67 | 68 | return 0 69 | end 70 | 71 | local function docs_update() 72 | local doc_manager = require('tarantool-lsp.tnt-doc.doc-manager') 73 | local ok, err = doc_manager.updateDoc({ 74 | doc_dir = fio.pathjoin(lsp_root, '.tnt_doc'), 75 | completion_dir = fio.pathjoin(lsp_root, 'completions') 76 | }) 77 | if not ok then 78 | log.error(err) 79 | return 1 80 | end 81 | 82 | return 0 83 | end 84 | 85 | local function docs() 86 | local docs_commands = { 87 | info = docs_info, 88 | init = docs_init, 89 | update = docs_update 90 | } 91 | 92 | docs_commands[positional_arguments[1]](positional_arguments) 93 | end 94 | 95 | local commands = setmetatable({ 96 | server = { 97 | func = exit_wrapper(require('tarantool-lsp.loop')), 98 | help = { 99 | header = "%s server", 100 | description = "Start LSP server on listening (using for client purposes)" 101 | } 102 | }, 103 | ws = { 104 | func = require('tarantool-lsp.websocket'), 105 | help = { 106 | header = "%s ws", 107 | description = "Start WebSocket LSP server on listening" 108 | } 109 | }, 110 | docs = { 111 | func = exit_wrapper(docs), 112 | help = { 113 | header = "%s docs [init|update|info]", 114 | description = "Module for docs management" 115 | }, 116 | subcommands = { 117 | init = { 118 | help = { 119 | header = "%s docs init", 120 | description = "Init documentation submodule" 121 | } 122 | }, 123 | update = { 124 | help = { 125 | header = "%s docs update", 126 | description = "Update documentation" 127 | } 128 | }, 129 | info = { 130 | help = { 131 | header = "%s docs info", 132 | description = "Get information about documentation" 133 | } 134 | } 135 | } 136 | } 137 | }, { 138 | __index = function() 139 | log.error("Unknown command '%s'", command_name) 140 | usage() 141 | end 142 | }) 143 | 144 | local function usage_command(name, cmd) 145 | local header = cmd.help.header 146 | -- if linkmode then 147 | -- header = cmd.help.linkmode 148 | -- end 149 | if type(header) == 'string' then 150 | header = { header } 151 | end 152 | for no, line in ipairs(header) do 153 | log.error(" " .. line, name) 154 | end 155 | end 156 | 157 | local function usage_header() 158 | log.error("Tarantool LSP CLI") 159 | end 160 | 161 | local function usage_commands(commands, verbose) 162 | local names = fun.iter(commands):map( 163 | function(self_name, cmd) return {self_name, cmd.help.weight or 0} end 164 | ):totable() 165 | table.sort(names, function(left, right) return left[2] < right[2] end) 166 | for _, cmd_name in ipairs(names) do 167 | local cmd = commands[cmd_name[1]] 168 | if cmd.help.deprecated ~= true then 169 | usage_command(self_name, cmd, false) 170 | if verbose then 171 | log.error("") 172 | log.error(cmd.help.description) 173 | end 174 | if cmd.subcommands then 175 | usage_commands(cmd.subcommands, verbose) 176 | end 177 | end 178 | end 179 | end 180 | 181 | usage = function(command, verbose) 182 | do -- in case a command is passed and is a valid command 183 | local command_struct = rawget(commands, command) 184 | if command ~= nil and command_struct then 185 | log.error("Usage:\n") 186 | usage_command(self_name, command_struct, true) 187 | log.error("") 188 | log.error(command_struct.help.description) 189 | os.exit(1) 190 | end 191 | end -- do this otherwise 192 | usage_header() 193 | if default_file ~= nil then 194 | log.error("Config file: %s", default_file) 195 | end 196 | log.error("") 197 | log.error("Usage:") 198 | usage_commands(commands, verbose) 199 | os.exit(1) 200 | end 201 | 202 | -- parse parameters and put the result into positional/keyword_arguments 203 | local function populate_arguments() 204 | -- returns the command name, file list and named parameters 205 | local function parameters_parse(parameters) 206 | local command_name = table.remove(parameters, 1) 207 | local positional_arguments, keyword_arguments = {}, {} 208 | for k, v in pairs(parameters) do 209 | if type(k) == 'number' then 210 | positional_arguments[k] = v 211 | else 212 | keyword_arguments[k] = v 213 | end 214 | end 215 | return command_name, positional_arguments, keyword_arguments 216 | end 217 | 218 | local parameters = argparse(arg, {}) 219 | 220 | local cmd_name 221 | cmd_name, positional_arguments, keyword_arguments = parameters_parse(parameters) 222 | if cmd_name == 'help' or parameters.help == true or #arg < 1 then 223 | usage(cmd_name, true) 224 | end 225 | 226 | keyword_arguments = parameters or {} 227 | end 228 | 229 | local function main() 230 | populate_arguments() 231 | local cmd_pair = commands[command_name] 232 | if cmd_pair.subcommands and #arg < 2 then 233 | log.error("Not enough arguments for '%s' command\n", command_name) 234 | usage(command_name) 235 | end 236 | 237 | cmd_pair.func() 238 | end 239 | 240 | main() 241 | -------------------------------------------------------------------------------- /editors.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | First of all, you need to make your text editor ready to work with LSP. 3 | Some editors support this protocol internally, others support it via plugins. 4 | 5 | ## Important note 6 | 7 | > backend - current LSP implementation for Tarantool/Lua (this repo). 8 | 9 | Anyway, you need to specify the path for the editor to start the LSP server. 10 | The editor will start a new process with the LSP backend, you don't need to 11 | start it manually. 12 | 13 | If you install the backend via a packet manager, you will get LSP installed and 14 | available from the default environment. 15 | ```bash 16 | # Command for the default LSP backend mode (stdin/stout) 17 | tarantool-lsp server 18 | 19 | # Command for the Websocket mode (see README.md) 20 | tarantool-lsp ws 21 | ``` 22 | 23 | See the [Examples](#Examples) section for more details. 24 | 25 | ## Editors 26 | 27 | ### Visual Studio Code 28 | 29 | Visual Studio Code implements language client support via an extension 30 | [library][vscode]. If you have a working configuration, please contribute it! 31 | 32 | [vscode]: https://www.npmjs.com/package/vscode-languageclient 33 | 34 | ### Atom-IDE 35 | 36 | Atom, like VS Code, implements language client support via an extension 37 | [library][atom-ide]. If you have a working configuration, please contribute it! 38 | 39 | [atom-ide]: https://github.com/atom/atom-languageclient 40 | 41 | ### Sublime Text 3 42 | 43 | Sublime has an [LSP plugin][st3]. See the [Examples](#Examples) section for 44 | default configuration. 45 | 46 | [st3]: https://github.com/tomv564/LSP 47 | 48 | ### Emacs 49 | 50 | Emacs has a [package][emacs] to create language clients. If you have a working 51 | configuration, please contribute it! 52 | 53 | [emacs]: https://github.com/emacs-lsp/lsp-mode 54 | 55 | ## Examples 56 | 57 | Default Sublime configuration looks like this: 58 | ```json 59 | { 60 | "clients": 61 | { 62 | "tarantool-lsp": 63 | { 64 | "command": 65 | [ 66 | "tarantool-lsp", 67 | "server" 68 | ], 69 | "enabled": true, 70 | "languageId": "lua", 71 | "scopes": [ 72 | "source.lua" 73 | ], 74 | "syntaxes": [ 75 | "Packages/Lua/Lua.sublime-syntax" 76 | ] 77 | } 78 | } 79 | } 80 | 81 | ``` 82 | 83 | For more details, please see the documentation for your editor -- or the editor's 84 | LSP plugin. 85 | -------------------------------------------------------------------------------- /gen_completion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf .tnt-doc 4 | rm -rf ./tarantool-lsp/completions 5 | ./bin/tarantool-lsp docs init 6 | mv ./completions ./tarantool-lsp/completions 7 | -------------------------------------------------------------------------------- /images/completion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/lua-lsp/cd7813222e58c07d251d9426d3f318146a883ade/images/completion.gif -------------------------------------------------------------------------------- /images/hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/lua-lsp/cd7813222e58c07d251d9426d3f318146a883ade/images/hover.gif -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # tarantool-lsp 2 | [![Build Status](https://travis-ci.org/artur-barsegyan/tarantool-lsp.svg)](https://travis-ci.org/tarantool/lua-lsp) 3 | 4 | A [Language Server][lsp] for Tarantool/Lua code, written in Lua. 5 | 6 | [lsp]: https://github.com/Microsoft/language-server-protocol 7 | 8 | It is still a work in progress, but quite usable. It currently 9 | supports: 10 | 11 | ## [lua-lsp legacy][lua-lsp] 12 | 13 | [lua-lsp]: https://github.com/tarantool/lua-lsp 14 | 15 | * Limited auto-completion 16 | * Goto definition 17 | * As you type linting and syntax checking 18 | * Code formatting 19 | * Lua 5.1 20 | 21 | ## Tarantool specific 22 | * Autocompletion for Tarantool built-in libs on the fly: 23 | 24 | ![Completion](./images/completion.gif) 25 | 26 | * Hovering for Tarantool built-in libs on the fly: 27 | 28 | ![Hover](./images/hover.gif) 29 | 30 | * Enhanced completion: 31 | - [NEW] Completions triggered only on `require` 32 | 33 | * Implementation CLI doc manager: 34 | 35 | * parsing Tarantool documentation 36 | * version management 37 | * manual updates 38 | 39 | * Powered by Tarantool 40 | 41 | ### Installation/Usage 42 | 43 | `tarantool-lsp` can be installed using `brew`: 44 | ``` 45 | $ brew install https://github.com/tarantool/lua-lsp/raw/master/tarantoollsp.rb --HEAD 46 | ``` 47 | This will install the `tarantool-lsp`. 48 | 49 | To enable Tarantool documentation for the server, say: 50 | 51 | ``` 52 | $ tarantool-lsp docs init 53 | ``` 54 | 55 | To update the documentation, say: 56 | 57 | ``` 58 | $ tarantool-lsp docs update 59 | ``` 60 | 61 | After this, configure your text editor. Language clients can then communicate 62 | with this process using `stdio` as a transport. See [editors.md](editors.md) 63 | for more instructions specific to your editor of choice. 64 | 65 | ### Library 66 | 67 | You can use `tarantool-lsp` as a library for webserver with websocket. 68 | 69 | ``` 70 | tarantoolctl rocks install tarantool-lsp 71 | ``` 72 | 73 | In-code usage: 74 | 75 | ``` 76 | create_websocket_handler() 77 | ``` 78 | 79 | ### Plugins 80 | 81 | `tarantool-lsp` automatically integrates with common Lua packages when they are 82 | installed. For linting, install `luacheck`: 83 | ``` 84 | $ luarocks install luacheck 85 | ``` 86 | For code formatting, we currently support Formatter and LCF. Formatter is 5.1 87 | only, whereas LCF is 5.3 only. 88 | 89 | 5.1: 90 | ``` 91 | $ luarocks-5.1 install Formatter 92 | $ luarocks-5.3 install lcf 93 | ``` 94 | If you have some other package and you would like to see it integrated, 95 | feel free to leave an issue/PR. Other plugins are always welcome, especially 96 | if they provide materially different results. 97 | 98 | ### Configuration 99 | 100 | `tarantool-lsp` reads a few project-level configuration files to do its work. 101 | 102 | To configure linting, we read your standard [.luacheckrc][check] file. 103 | 104 | For auto-complete support, we reimplement the [.luacompleterc][complete] format 105 | created by `atom-autocomplete-lua`. In particular, we need `luaVersion` to 106 | properly understand your code. 107 | 108 | More LSP-specific configuration flags will hopefully be provided through your 109 | editor's configuration support. 110 | 111 | [complete]: https://github.com/dapetcu21/atom-autocomplete-lua#configuration 112 | [check]: http://luacheck.readthedocs.io/en/stable/config.html 113 | 114 | ### TODO 115 | 116 | The LSP spec is big, and we don't implement all of it. Here is a 117 | quick wishlist, roughly ordered by priority/feasibility. 118 | 119 | * List references (`textDocument/references`) 120 | * Find symbols (`workspace/symbol`) 121 | * Function signature help (`textDocument/signatureHelp`) 122 | * Code links (`textDocument/documentLink`) 123 | * File events (`workspace/didChangeWatchedFiles`) 124 | -------------------------------------------------------------------------------- /scripts/compile_data.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | --- Takes each template and makes a require()-compatible lua file from it 4 | -- so we can avoid wierd datafile resolving issues 5 | local templates = { 6 | "https://raw.githubusercontent.com/rm-code/love-atom/master/data/love-completions.json", 7 | "https://raw.githubusercontent.com/dapetcu21/atom-autocomplete-lua/master/lib/stdlib/5_1.json", 8 | "https://raw.githubusercontent.com/dapetcu21/atom-autocomplete-lua/master/lib/stdlib/5_2.json", 9 | "https://raw.githubusercontent.com/dapetcu21/atom-autocomplete-lua/master/lib/stdlib/5_3.json", 10 | "https://raw.githubusercontent.com/dapetcu21/atom-autocomplete-lua/master/lib/stdlib/luajit-2_0.json", 11 | } 12 | 13 | local json = require 'json' 14 | local serpent = require 'serpent' 15 | 16 | local function basename(url) 17 | return url:match("([^/.]+).json$") 18 | end 19 | 20 | os.execute("sh -c 'rm -rv lua-lsp/data/*.json'") 21 | for _, url in ipairs(templates) do 22 | os.execute(string.format("wget -P 'lua-lsp/data' -- %q", url)) 23 | 24 | local path = basename(url) 25 | io.stderr:write("generating " .. path .. "\n") 26 | 27 | local f = assert(io.open("lua-lsp/data/"..path..".json")) 28 | local s = json.decode(f:read("*a")) 29 | f:close() 30 | 31 | f = assert(io.open("lua-lsp/data/"..path..".lua", 'w')) 32 | -- FIXME: ser does not produce stable results 33 | -- this makes git diff/blame a lot less useful, so we should switch to 34 | -- another loadstring compatible serializer 35 | f:write("return ", serpent.block(s, {comment = false})) 36 | f:close() 37 | end 38 | os.execute("sh -c 'rm -rv lua-lsp/data/*.json'") 39 | -------------------------------------------------------------------------------- /scripts/install_hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ln -sv ../../scripts/pre-commit.sh .git/hooks/pre-commit 3 | ln -sv ../../scripts/pre-push.sh .git/hooks/pre-push 4 | -------------------------------------------------------------------------------- /scripts/observe.lua: -------------------------------------------------------------------------------- 1 | -- this tool logs types that pass in and out of functions to generate automatic 2 | -- function signatures. These will often be dumb and redundant but that's okay! 3 | -- these should be used as a starting point, like much autogenerated knowledge. 4 | -- 5 | 6 | local db = {} 7 | 8 | local function pack(...) 9 | return {n=select('#', ...), ...} 10 | end 11 | 12 | local function save_table(def, tbl) 13 | def.fields = def.fields or {} 14 | local fields = def.fields 15 | for k, v in pairs(tbl) do 16 | local t = type(v) 17 | fields[k] = fields[k] or {n=0} 18 | fields[k].n = fields[k].n + 1 19 | if t == "table" then 20 | fields[k].table = fields[k].table or {fields={}} 21 | save_table(fields[k].table, v) 22 | else 23 | fields[k][t] = (fields[k][t] or 0) + 1 24 | end 25 | end 26 | end 27 | 28 | local observe = {} 29 | function observe.wrapfn(fn, key) 30 | key = key or fn 31 | return function(...) 32 | db[key] = db[key] or {call={n=0}, ret={n=0}} 33 | local call = db[key].call 34 | local ret = db[key].ret 35 | call.n = call.n + 1 36 | for i=1, select('#', ...) do 37 | local t = type(select(i, ...)) 38 | call[i] = call[i] or {n=0} 39 | call[i].n = call[i].n + 1 40 | if t == "table" then 41 | call[i][t] = call[i][t] or {} 42 | save_table(call[i].table, select(i, ...)) 43 | else 44 | call[i][t] = (call[i][t] or 0) + 1 45 | end 46 | end 47 | local r = pack(fn(...)) 48 | ret.n = ret.n + 1 49 | for i=1, r.n do 50 | local t = type(r[i]) 51 | ret[i] = ret[i] or {n=0} 52 | ret[i].n = ret[i].n + 1 53 | if t == "table" then 54 | ret[i][t] = ret[i][t] or {} 55 | save_table(ret[i].table, r[i]) 56 | else 57 | ret[i][t] = (ret[i][t] or 0) + 1 58 | end 59 | end 60 | return unpack(r, 1, r.n) 61 | end 62 | end 63 | 64 | function observe.save(fname) 65 | io.stderr:write(require'inspect'(db).."\n") 66 | end 67 | 68 | function observe.reset() 69 | db = {} 70 | end 71 | return observe 72 | -------------------------------------------------------------------------------- /scripts/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # check for syntax/whitespace errors 4 | luacheck . --only 0 6 5 | -------------------------------------------------------------------------------- /scripts/pre-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # check for simple luacheck errors 6 | #luacheck . 7 | 8 | # Check if we actually have commits to push 9 | #commits=`git log @{u}..` 10 | if [ -z "$commits" ]; then 11 | exit 0 12 | fi 13 | 14 | LUAROCKS="luarocks-5.1" 15 | LUA="luajit" 16 | TREE="/tmp/lsptest" 17 | eval $($LUAROCKS path --tree="$TREE") 18 | $LUAROCKS make --tree="$TREE" 19 | cd "$TREE/.." 20 | $LUA -l tarantool-lsp.loop -e "print('loaded succesfully!')os.exit(0)" 21 | rm -r "$TREE" 22 | -------------------------------------------------------------------------------- /scripts/watch_busted.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/fish 2 | while true; clear; sleep .21s; busted --verbose $argv; inotifywait -qr .; end 3 | -------------------------------------------------------------------------------- /spec/complete_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | 3 | local completionKinds = { 4 | Text = 1, 5 | Method = 2, 6 | Function = 3, 7 | Constructor = 4, 8 | Field = 5, 9 | Variable = 6, 10 | Class = 7, 11 | Interface = 8, 12 | Module = 9, 13 | Property = 10, 14 | Unit = 11, 15 | Value = 12, 16 | Enum = 13, 17 | Keyword = 14, 18 | Snippet = 15, 19 | Color = 16, 20 | File = 17, 21 | Reference = 18, 22 | } 23 | 24 | describe("textDocument/completion", function() 25 | it("returns nothing with no symbols", function() 26 | mock_loop(function(rpc) 27 | local text = "\n" 28 | local doc = { 29 | uri = "file:///tmp/fake.lua" 30 | } 31 | rpc.notify("textDocument/didOpen", { 32 | textDocument = {uri = doc.uri, text = text} 33 | }) 34 | local callme 35 | rpc.request("textDocument/completion", { 36 | textDocument = doc, 37 | position = {line = 0, character = 0} 38 | }, function(out) 39 | assert.same({ 40 | isIncomplete = false, 41 | items = {} 42 | }, out) 43 | callme = true 44 | end) 45 | assert.truthy(callme) 46 | end) 47 | end) 48 | 49 | it("returns local variables", function() 50 | mock_loop(function(rpc) 51 | local text = "local mySymbol\nreturn m" 52 | local doc = { 53 | uri = "file:///tmp/fake.lua" 54 | } 55 | rpc.notify("textDocument/didOpen", { 56 | textDocument = {uri = doc.uri, text = text} 57 | }) 58 | local callme 59 | rpc.request("textDocument/completion", { 60 | textDocument = doc, 61 | position = {line = 1, character = 8} 62 | }, function(out) 63 | table.sort(out.items, function(a, b) 64 | return a < b 65 | end) 66 | assert.same({ 67 | isIncomplete = false, 68 | items = { 69 | {label = "mySymbol", kind = completionKinds.Variable} 70 | } 71 | }, out) 72 | callme = true 73 | end) 74 | assert.truthy(callme) 75 | 76 | callme = nil 77 | text = "local symbolA, symbolB\n return s" 78 | rpc.notify("textDocument/didChange", { 79 | textDocument = {uri = doc.uri}, 80 | contentChanges = {{text = text}} 81 | }) 82 | rpc.request("textDocument/completion", { 83 | textDocument = doc, 84 | position = {line = 1, character = 9} 85 | }, function(out) 86 | table.sort(out.items, function(a, b) 87 | return a.label < b.label 88 | end) 89 | assert.same({ 90 | isIncomplete = false, 91 | items = { 92 | {label = "symbolA", kind = completionKinds.Variable}, 93 | {label="symbolB", kind = completionKinds.Variable} 94 | } 95 | }, out) 96 | callme = true 97 | end) 98 | assert.truthy(callme) 99 | 100 | callme = nil 101 | text = "local symbolC \nlocal s\n local symbolD" 102 | rpc.notify("textDocument/didChange", { 103 | textDocument = {uri = doc.uri}, 104 | contentChanges = {{text = text}} 105 | }) 106 | rpc.request("textDocument/completion", { 107 | textDocument = doc, 108 | position = {line = 1, character = 7} 109 | }, function(out) 110 | table.sort(out.items, function(a, b) 111 | return a < b 112 | end) 113 | assert.same({ 114 | isIncomplete = false, 115 | items = { 116 | {label = "symbolC", kind = completionKinds.Variable} 117 | } 118 | }, out) 119 | callme = true 120 | end) 121 | assert.truthy(callme) 122 | end) 123 | end) 124 | 125 | it("returns table fields", function() 126 | mock_loop(function(rpc) 127 | local text = [[ 128 | local tbl={} 129 | tbl.string='a' 130 | tbl.number=42 131 | return t 132 | ]] 133 | local doc = { 134 | uri = "file:///tmp/fake.lua" 135 | } 136 | rpc.notify("textDocument/didOpen", { 137 | textDocument = {uri = doc.uri, text = text} 138 | }) 139 | local callme 140 | rpc.request("textDocument/completion", { 141 | textDocument = doc, 142 | position = {line = 3, character = 8} 143 | }, function(out) 144 | table.sort(out.items, function(a, b) 145 | return a < b 146 | end) 147 | assert.equal(1, #out.items) 148 | assert.same({ 149 | detail = '', 150 | label = 'tbl', 151 | kind = completionKinds.Variable, 152 | }, out.items[1]) 153 | callme = true 154 | end) 155 | assert.truthy(callme) 156 | 157 | callme = nil 158 | text = text:gsub("\n$","bl.st\n") 159 | rpc.notify("textDocument/didChange", { 160 | textDocument = {uri = doc.uri}, 161 | contentChanges = {{text = text}} 162 | }) 163 | rpc.request("textDocument/completion", { 164 | textDocument = doc, 165 | position = {line = 3, character = 12} 166 | }, function(out) 167 | table.sort(out.items, function(a, b) 168 | return a.label < b.label 169 | end) 170 | assert.same({ 171 | isIncomplete = false, 172 | items = {{ 173 | detail = '"a"', 174 | label = "string", 175 | kind = completionKinds.Variable 176 | }} 177 | }, out) 178 | callme = true 179 | end) 180 | assert.truthy(callme) 181 | callme = nil 182 | rpc.request("textDocument/completion", { 183 | textDocument = doc, 184 | position = {line = 3, character = 12} 185 | }, function(out) 186 | table.sort(out.items, function(a, b) 187 | return a.label < b.label 188 | end) 189 | assert.same({ 190 | isIncomplete = false, 191 | items = {{ 192 | detail = '"a"', 193 | label = "string", 194 | kind = completionKinds.Variable 195 | }} 196 | }, out) 197 | callme = true 198 | end) 199 | assert.truthy(callme) 200 | end) 201 | end) 202 | 203 | it("resolves modules", function() 204 | mock_loop(function(rpc) 205 | local text = [[ 206 | local tbl=require'mymod' 207 | print(t) 208 | return tbl.a 209 | ]] 210 | local doc = { 211 | uri = "file:///tmp/fake.lua" 212 | } 213 | rpc.notify("textDocument/didOpen", { 214 | textDocument = {uri = doc.uri, text = text} 215 | }) 216 | local callme 217 | rpc.request("textDocument/completion", { 218 | textDocument = doc, 219 | position = {line = 1, character = 7} 220 | }, function(out) 221 | table.sort(out.items, function(a, b) 222 | return a < b 223 | end) 224 | assert.equal(1, #out.items) 225 | assert.same({ 226 | detail = 'M', 227 | label = 'tbl', 228 | kind = completionKinds.Module, 229 | }, out.items[1]) 230 | callme = true 231 | end) 232 | assert.truthy(callme) 233 | end) 234 | end) 235 | 236 | it("resolves strings", function() 237 | mock_loop(function(rpc) 238 | local text = [[ 239 | string = {test_example = true} 240 | local mystr = "" 241 | return mystr.t 242 | ]] 243 | local doc = { 244 | uri = "file:///tmp/fake.lua" 245 | } 246 | rpc.notify("textDocument/didOpen", { 247 | textDocument = {uri = doc.uri, text = text} 248 | }) 249 | local callme 250 | rpc.request("textDocument/completion", { 251 | textDocument = doc, 252 | position = {line = 2, character = 13} 253 | }, function(out) 254 | table.sort(out.items, function(a, b) 255 | return a < b 256 | end) 257 | assert.equal(1, #out.items) 258 | assert.same({ 259 | detail = 'True', 260 | label = 'test_example', 261 | kind = completionKinds.Variable, 262 | }, out.items[1]) 263 | callme = true 264 | end) 265 | assert.truthy(callme) 266 | end, {"_test"}) 267 | end) 268 | 269 | it("resolves calls to setmetatable", function() 270 | mock_loop(function(rpc) 271 | local text = [[ 272 | local mytbl = setmetatable({jeff=1}, {}) 273 | return mytbl.j 274 | ]] 275 | local doc = { 276 | uri = "file:///tmp/fake.lua" 277 | } 278 | rpc.notify("textDocument/didOpen", { 279 | textDocument = {uri = doc.uri, text = text} 280 | }) 281 | local callme 282 | rpc.request("textDocument/completion", { 283 | textDocument = doc, 284 | position = {line = 1, character = 13} 285 | }, function(out) 286 | table.sort(out.items, function(a, b) 287 | return a < b 288 | end) 289 | assert.equal(1, #out.items) 290 | assert.same({ 291 | detail = '1', 292 | label = 'jeff', 293 | kind = completionKinds.Variable, 294 | }, out.items[1]) 295 | callme = true 296 | end) 297 | assert.truthy(callme) 298 | end, {"_test"}) 299 | end) 300 | 301 | it("does not resolve invalid/incorrect keys", function() 302 | mock_loop(function(rpc) 303 | local text = [[ 304 | -- comment 305 | return nonexistent.a 306 | ]] 307 | local doc = { 308 | uri = "file:///tmp/fake.lua" 309 | } 310 | rpc.notify("textDocument/didOpen", { 311 | textDocument = {uri = doc.uri, text = text} 312 | }) 313 | local callme 314 | rpc.request("textDocument/completion", { 315 | textDocument = doc, 316 | position = {line = 0, character = 0} 317 | }, function(out) 318 | assert.equal(0, #out.items) 319 | callme = true 320 | end) 321 | assert.truthy(callme) 322 | 323 | callme = false 324 | rpc.request("textDocument/completion", { 325 | textDocument = doc, 326 | position = {line = 1, character = 19} 327 | }, function(out) 328 | assert.equal(0, #out.items) 329 | callme = true 330 | end) 331 | assert.truthy(callme) 332 | end, {"_test"}) 333 | end) 334 | 335 | it("can resolve varargs", function() 336 | mock_loop(function(rpc) 337 | local text = [[ 338 | function my_fun(...) 339 | return { ... } 340 | end 341 | return my_f 342 | ]] 343 | local doc = { 344 | uri = "file:///tmp/fake.lua" 345 | } 346 | rpc.notify("textDocument/didOpen", { 347 | textDocument = {uri = doc.uri, text = text} 348 | }) 349 | local callme 350 | rpc.request("textDocument/completion", { 351 | textDocument = doc, 352 | position = {line = 3, character = 11} 353 | }, function(out) 354 | table.sort(out.items, function(a, b) 355 | return a < b 356 | end) 357 | assert.equal(1, #out.items) 358 | assert.same({ 359 | detail = '', 360 | label = 'my_fun(...) ->
', 361 | insertText = 'my_fun', 362 | kind = completionKinds.Function, 363 | }, out.items[1]) 364 | callme = true 365 | end) 366 | assert.truthy(callme) 367 | end) 368 | end) 369 | 370 | 371 | it("can resolve simple function returns", function() 372 | mock_loop(function(rpc) 373 | local text = [[ 374 | function my_fun() 375 | return { field = "a" } 376 | end 377 | local mytbl = my_fun() 378 | return mytbl.f 379 | ]] 380 | local doc = { 381 | uri = "file:///tmp/fake.lua" 382 | } 383 | rpc.notify("textDocument/didOpen", { 384 | textDocument = {uri = doc.uri, text = text} 385 | }) 386 | local callme 387 | rpc.request("textDocument/completion", { 388 | textDocument = doc, 389 | position = {line = 4, character = 13} 390 | }, function(out) 391 | table.sort(out.items, function(a, b) 392 | return a < b 393 | end) 394 | assert.equal(1, #out.items) 395 | assert.same({ 396 | detail = '"a"', 397 | label = 'field', 398 | kind = completionKinds.Variable, 399 | }, out.items[1]) 400 | callme = true 401 | end) 402 | assert.truthy(callme) 403 | end) 404 | end) 405 | 406 | pending("can handle function return modification", function() 407 | mock_loop(function(rpc) 408 | local text = [[ 409 | function my_fun() 410 | return { field = "a" } 411 | end 412 | local mytbl = my_fun() mytbl.jeff = "b" 413 | return mytbl.j 414 | ]] 415 | local doc = { 416 | uri = "file:///tmp/fake.lua" 417 | } 418 | rpc.notify("textDocument/didOpen", { 419 | textDocument = {uri = doc.uri, text = text} 420 | }) 421 | local callme 422 | rpc.request("textDocument/completion", { 423 | textDocument = doc, 424 | position = {line = 4, character = 14} 425 | }, function(out) 426 | table.sort(out.items, function(a, b) 427 | return a.label < b.label 428 | end) 429 | assert.equal(1, #out.items) 430 | assert.same({ 431 | detail = '"b"', 432 | label = 'jeff' 433 | }, out.items[1]) 434 | callme = true 435 | end) 436 | assert.truthy(callme) 437 | end) 438 | end) 439 | 440 | pending("can resolve inline function returns", function() 441 | mock_loop(function(rpc) 442 | local text = [[ 443 | function my_fun() 444 | return { field = "a" } 445 | end 446 | return my_fun().f 447 | ]] 448 | local doc = { 449 | uri = "file:///tmp/fake.lua" 450 | } 451 | rpc.notify("textDocument/didOpen", { 452 | textDocument = {uri = doc.uri, text = text} 453 | }) 454 | local callme 455 | rpc.request("textDocument/completion", { 456 | textDocument = doc, 457 | position = {line = 4, character = 13} 458 | }, function(out) 459 | table.sort(out.items, function(a, b) 460 | return a < b 461 | end) 462 | assert.equal(1, #out.items) 463 | assert.same({ 464 | detail = 'M', 465 | label = 'tbl' 466 | }, out.items[1]) 467 | callme = true 468 | end) 469 | assert.truthy(callme) 470 | end) 471 | end) 472 | end) 473 | -------------------------------------------------------------------------------- /spec/definition_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | 3 | local function not_empty(t) 4 | assert.not_same({}, t) 5 | end 6 | 7 | describe("textDocument/didOpen", function() 8 | it("triggers", function() 9 | mock_loop(function(rpc, s_rpc) 10 | spy.on(s_rpc, "notify") 11 | local methods = require 'tarantool-lsp.methods' 12 | local opened = spy.on(methods, "textDocument/didOpen") 13 | local text = "local jeff = nil \n return jeff" 14 | rpc.notify("textDocument/didOpen", { 15 | textDocument = { 16 | uri = "file:///tmp/fake.lua", 17 | text = text 18 | } 19 | }) 20 | assert.truthy(Documents) 21 | assert.truthy(Documents["file:///tmp/fake.lua"]) 22 | assert.spy(s_rpc.notify).was_called() 23 | assert.spy(opened).was_called() 24 | assert.equal(text, Documents["file:///tmp/fake.lua"].text) 25 | end) 26 | end) 27 | end) 28 | 29 | describe("textDocument/definition", function() 30 | it("returns a definition", function() 31 | mock_loop(function(rpc) 32 | local text = "local jeff = nil \n return jeff\n" 33 | local doc = { 34 | uri = "file:///tmp/fake.lua" 35 | } 36 | rpc.notify("textDocument/didOpen", { 37 | textDocument = {uri = doc.uri, text = text} 38 | }) 39 | local callme 40 | rpc.request("textDocument/definition", { 41 | textDocument = doc, 42 | position = { line = 1, character = 8} 43 | }, function() 44 | callme = true 45 | end) 46 | assert.truthy(callme) 47 | end) 48 | end) 49 | 50 | it("returns a definition near EOF", function() 51 | -- is the last character in the file next to the EOF marker? aka, no 52 | -- trailing newline 53 | mock_loop(function(rpc) 54 | local text = "local jeff = nil \n return jeff" 55 | local doc = { 56 | uri = "file:///tmp/fake.lua" 57 | } 58 | rpc.notify("textDocument/didOpen", { 59 | textDocument = {uri = doc.uri, text = text} 60 | }) 61 | local callme 62 | rpc.request("textDocument/definition", { 63 | textDocument = doc, 64 | position = { line = 1, character = 8} 65 | }, function() 66 | callme = true 67 | end) 68 | assert.truthy(callme) 69 | end) 70 | end) 71 | 72 | it("handles multiple symbols #tmp", function() 73 | mock_loop(function(rpc) 74 | local text = "local jack\nlocal diane\n return jack, diane\n" 75 | local doc = { 76 | uri = "file:///tmp/fake.lua" 77 | } 78 | rpc.notify("textDocument/didOpen", { 79 | textDocument = {uri = doc.uri, text = text} 80 | }) 81 | rpc.request("textDocument/definition", { 82 | textDocument = doc, 83 | position = {line = 2, character = 8} -- jack 84 | }, function(out) 85 | not_empty(out) 86 | assert.same({line=0, character=6}, out.range.start) 87 | end) 88 | 89 | rpc.request("textDocument/definition", { 90 | textDocument = doc, 91 | position = {line = 2, character = 14} -- diane 92 | }, function(out) 93 | not_empty(out) 94 | assert.same({line=1, character=6}, out.range.start) 95 | end) 96 | end) 97 | end) 98 | 99 | it("handles symbol redefinition", function() 100 | mock_loop(function(rpc) 101 | -- This should return the first jack on line 1 and the second jack 102 | -- on line 3. This means there is an implicit scope-within-a-scope 103 | -- we need to implement 104 | local text = "local jack\njack()\nlocal jack\n return jack\n" 105 | local doc = { 106 | uri = "file:///tmp/fake.lua" 107 | } 108 | rpc.notify("textDocument/didOpen", { 109 | textDocument = {uri = doc.uri, text = text} 110 | }) 111 | 112 | rpc.request("textDocument/definition", { 113 | textDocument = doc, 114 | position = {line = 3, character = 8} -- jackv2 115 | }, function(out) 116 | not_empty(out) 117 | assert.same({line=2, character=6}, out.range.start) 118 | end) 119 | 120 | rpc.request("textDocument/definition", { 121 | textDocument = doc, 122 | position = {line = 1, character = 0} -- jackv1 123 | }, function(out) 124 | not_empty(out) 125 | assert.same({line=0, character=6}, out.range.start) 126 | end) 127 | end) 128 | end) 129 | 130 | it("handles field definition", function() 131 | mock_loop(function(rpc) 132 | local text = [[ 133 | local jack = { diane = 1 } 134 | jack.jill = 2 135 | return jack.diane + jack.jill 136 | ]] 137 | local doc = { 138 | uri = "file:///tmp/fake.lua" 139 | } 140 | rpc.notify("textDocument/didOpen", { 141 | textDocument = {uri = doc.uri, text = text} 142 | }) 143 | rpc.request("textDocument/definition", { 144 | textDocument = doc, 145 | position = {line=2, character=12} -- jack.diane 146 | }, function(out) 147 | not_empty(out) 148 | assert.same({line=0, character=15}, out.range.start) 149 | end) 150 | 151 | rpc.request("textDocument/definition", { 152 | textDocument = doc, 153 | position = {line = 2, character = 25} -- jack.jill 154 | }, function(out) 155 | not_empty(out) 156 | assert.same({line=1, character=5}, out.range.start) 157 | end) 158 | end) 159 | end) 160 | 161 | it("handles function fields", function() 162 | mock_loop(function(rpc) 163 | local text = [[ 164 | local jack = { diane = function() end } 165 | function jack.jill() return end 166 | function jack:little_ditty() return end 167 | return jack.diane() + jack.jill() + jack:little_ditty() 168 | ]] 169 | local doc = { 170 | uri = "file:///tmp/fake.lua" 171 | } 172 | rpc.notify("textDocument/didOpen", { 173 | textDocument = {uri = doc.uri, text = text} 174 | }) 175 | 176 | rpc.request("textDocument/definition", { 177 | textDocument = doc, 178 | position = {line=3, character=12} -- jack.diane 179 | }, function(out) 180 | not_empty(out) 181 | assert.same({line=0, character=15}, out.range.start) 182 | end) 183 | 184 | rpc.request("textDocument/definition", { 185 | textDocument = doc, 186 | position = {line = 3, character = 27} -- jack.jill 187 | }, function(out) 188 | assert.same({line=1, character=14}, out.range.start) 189 | end) 190 | 191 | rpc.request("textDocument/definition", { 192 | textDocument = doc, 193 | position = {line=3, character=41} -- jack:little_ditty 194 | }, function(out) 195 | not_empty(out) 196 | assert.same({line=2, character=14}, out.range.start) 197 | end) 198 | end) 199 | end) 200 | 201 | 202 | it("handles wide characters", function() 203 | mock_loop(function(rpc) 204 | local text = "a='😬' local jeff\n return jeff\n" 205 | local doc = { 206 | uri = "file:///tmp/fake.lua" 207 | } 208 | rpc.notify("textDocument/didOpen", { 209 | textDocument = {uri = doc.uri, text = text} 210 | }) 211 | 212 | rpc.request("textDocument/definition", { 213 | textDocument = doc, 214 | position = {line = 1, character = 8} -- jackv2 215 | }, function(out) 216 | not_empty(out) 217 | assert.same({line=0, character=13}, out.range.start) 218 | end) 219 | end) 220 | end) 221 | 222 | it("handles tabs", function() 223 | mock_loop(function(rpc) 224 | local text = "\tlocal jeff\n return jeff\n" 225 | local doc = { 226 | uri = "file:///tmp/fake.lua" 227 | } 228 | rpc.notify("textDocument/didOpen", { 229 | textDocument = {uri = doc.uri, text = text} 230 | }) 231 | 232 | rpc.request("textDocument/definition", { 233 | textDocument = doc, 234 | position = {line = 1, character = 8} -- jackv2 235 | }, function(out) 236 | not_empty(out) 237 | assert.same({line=0, character=7}, out.range.start) 238 | end) 239 | end) 240 | end) 241 | 242 | it("handles multivars", function() 243 | mock_loop(function(rpc) 244 | local text = "local jack, diane = 1, 2\nprint(diane)\nreturn jack\n" 245 | local doc = { 246 | uri = "file:///tmp/fake.lua" 247 | } 248 | rpc.notify("textDocument/didOpen", { 249 | textDocument = {uri = doc.uri, text = text} 250 | }) 251 | 252 | rpc.request("textDocument/definition", { 253 | textDocument = doc, 254 | position = {line = 1, character = 8} -- diane 255 | }, function(out) 256 | assert.same({line=0, character=12}, out.range.start) 257 | end) 258 | 259 | rpc.request("textDocument/definition", { 260 | textDocument = doc, 261 | position = {line = 2, character = 8} -- jackv2 262 | }, function(out) 263 | not_empty(out) 264 | assert.same({line=0, character=6}, out.range.start) 265 | end) 266 | end) 267 | end) 268 | 269 | it("handles document jumping", function() 270 | mock_loop(function(rpc) 271 | local text = [[ 272 | local a = {} 273 | a.jeff = 1 274 | return a 275 | ]] 276 | local doc = { 277 | uri = "file:///tmp/fake1.lua" 278 | } 279 | rpc.notify("textDocument/didOpen", { 280 | textDocument = {uri = doc.uri, text = text} 281 | }) 282 | 283 | text = [[ 284 | local a = require'fake1' 285 | return a.jeff 286 | ]] 287 | local doc1 = doc 288 | doc = { 289 | uri = "file:///tmp/fake2.lua" 290 | } 291 | rpc.notify("textDocument/didOpen", { 292 | textDocument = {uri = doc.uri, text = text} 293 | }) 294 | 295 | rpc.request("textDocument/definition", { 296 | textDocument = doc, 297 | position = {line = 1, character = 10} -- a.jeff 298 | }, function(out) 299 | assert.equal(doc1.uri, out.uri) 300 | assert.same({line=1, character=2}, out.range.start) 301 | end) 302 | 303 | rpc.request("textDocument/definition", { 304 | textDocument = doc, 305 | position = {line = 1, character = 8} -- a 306 | }, function(out) 307 | assert.equal(doc.uri, out.uri) 308 | assert.same({line=0, character=6}, out.range.start) 309 | end) 310 | end) 311 | end) 312 | 313 | it("handles missing documents ", function() 314 | mock_loop(function(rpc) 315 | local text = [[ 316 | local a = require'fake1' 317 | return a.jeff 318 | ]] 319 | local doc = { 320 | uri = "file:///tmp/fake2.lua" 321 | } 322 | rpc.notify("textDocument/didOpen", { 323 | textDocument = {uri = doc.uri, text = text} 324 | }) 325 | 326 | --[[ FIXME: This currently produces an error, which is bad. 327 | rpc.request("textDocument/definition", { 328 | textDocument = doc, 329 | position = {line = 1, character = 10} -- a.jeff 330 | }, function(out) 331 | assert.equal(doc1.uri, out.uri) 332 | assert.same({line=1, character=2}, out.range.start) 333 | end) 334 | --]] 335 | 336 | rpc.request("textDocument/definition", { 337 | textDocument = doc, 338 | position = {line = 1, character = 8} -- a 339 | }, function(out) 340 | assert.equal(doc.uri, out.uri) 341 | assert.same({line=0, character=6}, out.range.start) 342 | end) 343 | end) 344 | end) 345 | end) 346 | -------------------------------------------------------------------------------- /spec/format_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | local fmt = require 'tarantool-lsp.formatting' 3 | 4 | local match = string.match 5 | local function trim(s) 6 | return match(s,'^()%s*$') and '' or match(s,'^%s*(.*%S)') 7 | end 8 | 9 | 10 | describe("textDocument/formatting", function() 11 | if fmt.driver == "noop" then 12 | pending("can't run: No formatter installed", function() end) 13 | return 14 | end 15 | 16 | it("works", function() 17 | mock_loop(function(rpc) 18 | -- luacheck: push ignore 19 | local text = [[ 20 | local function no_indent() 21 | if great then 22 | print(nice) 23 | else 24 | print() 25 | end 26 | end 27 | ]] 28 | local formatted = [[ 29 | local function no_indent() 30 | if great then 31 | print(nice) 32 | else 33 | print() 34 | end 35 | end 36 | ]] 37 | -- luacheck: pop 38 | local doc = { 39 | uri = "file:///tmp/fake.lua" 40 | } 41 | rpc.notify("textDocument/didOpen", { 42 | textDocument = {uri = doc.uri, text = text} 43 | }) 44 | local callme 45 | rpc.request("textDocument/formatting", { 46 | textDocument = doc, 47 | options = {tabSize = 4, insertSpaces = true} 48 | }, function(out) 49 | out = out[1] 50 | assert.same({ 51 | start={line=0, character=0}, 52 | ["end"]={line=6, character=3} 53 | }, out.range) 54 | -- LCF does not do a final empty line, so trim to get rid of 55 | -- the difference 56 | formatted = trim(formatted) 57 | out.newText = trim(out.newText) 58 | assert.same(formatted, out.newText) 59 | callme = true 60 | end) 61 | assert.truthy(callme) 62 | end) 63 | end) 64 | end) 65 | describe("textDocument/rangeFormatting", function() 66 | if fmt.driver == "noop" then 67 | pending("can't run: No formatter installed", function() end) 68 | return 69 | end 70 | 71 | it("works", function() 72 | mock_loop(function(rpc) 73 | -- luacheck: push ignore 74 | local text = [[ 75 | local function no_indent() 76 | if great then 77 | print(nice) 78 | else 79 | print() 80 | end 81 | end 82 | ]] 83 | local formatted = [[ 84 | local function no_indent() 85 | if great then 86 | print(nice) 87 | ]] 88 | -- luacheck: pop 89 | local doc = { 90 | uri = "file:///tmp/fake.lua" 91 | } 92 | rpc.notify("textDocument/didOpen", { 93 | textDocument = {uri = doc.uri, text = text} 94 | }) 95 | local callme 96 | rpc.request("textDocument/rangeFormatting", { 97 | textDocument = doc, 98 | options = {tabSize = 4, insertSpaces = true}, 99 | range = { 100 | start={line=0, character=0}, 101 | ["end"]={line=2, character=15} 102 | }, 103 | }, function(out) 104 | out = out[1] 105 | assert.same(formatted, out.newText) 106 | assert.same({ 107 | start={line=0, character=0}, 108 | ["end"]={line=2, character=15} 109 | }, out.range) 110 | callme = true 111 | end) 112 | assert.truthy(callme) 113 | end) 114 | end) 115 | end) 116 | -------------------------------------------------------------------------------- /spec/hover_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | 3 | describe("textDocument/hover", function() 4 | it("handles string returns", function() 5 | mock_loop(function(rpc) 6 | local text = [[ 7 | local function myfun() 8 | return "hi" 9 | end 10 | ]] 11 | local doc = { 12 | uri = "file:///tmp/fake.lua" 13 | } 14 | rpc.notify("textDocument/didOpen", { 15 | textDocument = {uri = doc.uri, text = text} 16 | }) 17 | local callme 18 | rpc.request("textDocument/hover", { 19 | textDocument = doc, 20 | position = {line = 0, character = 16} 21 | }, function(out) 22 | assert.same({ 23 | contents = {'myfun() -> "hi"\n'} 24 | }, out) 25 | callme = true 26 | end) 27 | assert.truthy(callme) 28 | end) 29 | end) 30 | 31 | it("handles number returns", function() 32 | mock_loop(function(rpc) 33 | local text = [[ 34 | local function myfun() 35 | return 42 36 | end 37 | ]] 38 | local doc = { 39 | uri = "file:///tmp/fake.lua" 40 | } 41 | rpc.notify("textDocument/didOpen", { 42 | textDocument = {uri = doc.uri, text = text} 43 | }) 44 | local callme 45 | rpc.request("textDocument/hover", { 46 | textDocument = doc, 47 | position = {line = 0, character = 16} 48 | }, function(out) 49 | assert.same({ 50 | contents = {'myfun() -> 42\n'} 51 | }, out) 52 | callme = true 53 | end) 54 | assert.truthy(callme) 55 | end) 56 | end) 57 | 58 | it("handles true returns", function() 59 | mock_loop(function(rpc) 60 | local text = [[ 61 | local function myfun() 62 | return true 63 | end 64 | ]] 65 | local doc = { 66 | uri = "file:///tmp/fake.lua" 67 | } 68 | rpc.notify("textDocument/didOpen", { 69 | textDocument = {uri = doc.uri, text = text} 70 | }) 71 | local callme 72 | rpc.request("textDocument/hover", { 73 | textDocument = doc, 74 | position = {line = 0, character = 16} 75 | }, function(out) 76 | assert.same({ 77 | contents = {'myfun() -> true\n'} 78 | }, out) 79 | callme = true 80 | end) 81 | assert.truthy(callme) 82 | end) 83 | end) 84 | 85 | it("handles false returns", function() 86 | mock_loop(function(rpc) 87 | local text = [[ 88 | local function myfun() 89 | return false 90 | end 91 | ]] 92 | local doc = { 93 | uri = "file:///tmp/fake.lua" 94 | } 95 | rpc.notify("textDocument/didOpen", { 96 | textDocument = {uri = doc.uri, text = text} 97 | }) 98 | local callme 99 | rpc.request("textDocument/hover", { 100 | textDocument = doc, 101 | position = {line = 0, character = 16} 102 | }, function(out) 103 | assert.same({ 104 | contents = {'myfun() -> false\n'} 105 | }, out) 106 | callme = true 107 | end) 108 | assert.truthy(callme) 109 | end) 110 | end) 111 | 112 | it("handles nil returns", function() 113 | mock_loop(function(rpc) 114 | local text = [[ 115 | local function myfun() 116 | return nil 117 | end 118 | ]] 119 | local doc = { 120 | uri = "file:///tmp/fake.lua" 121 | } 122 | rpc.notify("textDocument/didOpen", { 123 | textDocument = {uri = doc.uri, text = text} 124 | }) 125 | local callme 126 | rpc.request("textDocument/hover", { 127 | textDocument = doc, 128 | position = {line = 0, character = 16} 129 | }, function(out) 130 | assert.same({ 131 | contents = {'myfun() -> nil\n'} 132 | }, out) 133 | callme = true 134 | end) 135 | assert.truthy(callme) 136 | end) 137 | end) 138 | 139 | it("handles table returns #atm", function() 140 | mock_loop(function(rpc) 141 | local text = [[ 142 | local function myfun() 143 | return {} 144 | end 145 | ]] 146 | local doc = { 147 | uri = "file:///tmp/fake.lua" 148 | } 149 | rpc.notify("textDocument/didOpen", { 150 | textDocument = {uri = doc.uri, text = text} 151 | }) 152 | local callme 153 | rpc.request("textDocument/hover", { 154 | textDocument = doc, 155 | position = {line = 0, character = 16} 156 | }, function(out) 157 | assert.same({ 158 | contents = {'myfun() ->
\n'} 159 | }, out) 160 | callme = true 161 | end) 162 | assert.truthy(callme) 163 | end) 164 | end) 165 | 166 | it("handles function returns", function() 167 | mock_loop(function(rpc) 168 | local text = [[ 169 | local function myfun() 170 | return function() end 171 | end 172 | ]] 173 | local doc = { 174 | uri = "file:///tmp/fake.lua" 175 | } 176 | rpc.notify("textDocument/didOpen", { 177 | textDocument = {uri = doc.uri, text = text} 178 | }) 179 | local callme 180 | rpc.request("textDocument/hover", { 181 | textDocument = doc, 182 | position = {line = 0, character = 16} 183 | }, function(out) 184 | assert.same({ 185 | contents = {'myfun() -> \n'} 186 | }, out) 187 | callme = true 188 | end) 189 | assert.truthy(callme) 190 | end) 191 | end) 192 | 193 | it("handles named returns", function() 194 | mock_loop(function(rpc) 195 | local text = [[ 196 | local function myfun() 197 | local mystring = "hi" 198 | return mystring 199 | end 200 | ]] 201 | local doc = { 202 | uri = "file:///tmp/fake.lua" 203 | } 204 | rpc.notify("textDocument/didOpen", { 205 | textDocument = {uri = doc.uri, text = text} 206 | }) 207 | local callme 208 | rpc.request("textDocument/hover", { 209 | textDocument = doc, 210 | position = {line = 0, character = 16} 211 | }, function(out) 212 | assert.same({ 213 | contents = {'myfun() -> mystring\n'} 214 | }, out) 215 | callme = true 216 | end) 217 | assert.truthy(callme) 218 | end) 219 | end) 220 | 221 | it("handles multireturns", function() 222 | mock_loop(function(rpc) 223 | local text = [[ 224 | local function myfun() 225 | return 1, 2, 3 226 | end 227 | ]] 228 | local doc = { 229 | uri = "file:///tmp/fake.lua" 230 | } 231 | rpc.notify("textDocument/didOpen", { 232 | textDocument = {uri = doc.uri, text = text} 233 | }) 234 | local callme 235 | rpc.request("textDocument/hover", { 236 | textDocument = doc, 237 | position = {line = 0, character = 16} 238 | }, function(out) 239 | assert.same({ 240 | contents = {'myfun() -> 1, 2, 3\n'} 241 | }, out) 242 | callme = true 243 | end) 244 | assert.truthy(callme) 245 | end) 246 | end) 247 | 248 | it("handles multireturns with multiple sites", function() 249 | mock_loop(function(rpc) 250 | local text = [[ 251 | local function myfun() 252 | if ok then 253 | return ok 254 | else 255 | return nil, "oops" 256 | end 257 | end 258 | ]] 259 | local doc = { 260 | uri = "file:///tmp/fake.lua" 261 | } 262 | rpc.notify("textDocument/didOpen", { 263 | textDocument = {uri = doc.uri, text = text} 264 | }) 265 | local callme 266 | rpc.request("textDocument/hover", { 267 | textDocument = doc, 268 | position = {line = 0, character = 16} 269 | }, function(out) 270 | assert.same({ 271 | contents = {'myfun() -> ok | nil, "oops"\n'} 272 | }, out) 273 | callme = true 274 | end) 275 | assert.truthy(callme) 276 | end) 277 | end) 278 | 279 | it("deduplicates function return names", function() 280 | mock_loop(function(rpc) 281 | local text = [[ 282 | local function branchy() 283 | local myvar 284 | if true then 285 | return myvar 286 | else 287 | return myvar 288 | end 289 | end 290 | ]] 291 | local doc = { 292 | uri = "file:///tmp/fake.lua" 293 | } 294 | rpc.notify("textDocument/didOpen", { 295 | textDocument = {uri = doc.uri, text = text} 296 | }) 297 | local callme 298 | rpc.request("textDocument/hover", { 299 | textDocument = doc, 300 | position = {line = 0, character = 16} 301 | }, function(out) 302 | assert.same({ 303 | contents = {'branchy() -> myvar\n'} 304 | }, out) 305 | callme = true 306 | end) 307 | assert.truthy(callme) 308 | end) 309 | end) 310 | 311 | it("deduplicates function return literals", function() 312 | mock_loop(function(rpc) 313 | local text = [[ 314 | local function branchy() 315 | local myvar 316 | if true then 317 | return "yeah" 318 | else 319 | return "yeah" 320 | end 321 | return "nah" 322 | end 323 | ]] 324 | local doc = { 325 | uri = "file:///tmp/fake.lua" 326 | } 327 | rpc.notify("textDocument/didOpen", { 328 | textDocument = {uri = doc.uri, text = text} 329 | }) 330 | local callme 331 | rpc.request("textDocument/hover", { 332 | textDocument = doc, 333 | position = {line = 0, character = 16} 334 | }, function(out) 335 | assert.same({ 336 | contents = {'branchy() -> "yeah" | "nah"\n'} 337 | }, out) 338 | callme = true 339 | end) 340 | assert.truthy(callme) 341 | end) 342 | end) 343 | end) 344 | -------------------------------------------------------------------------------- /spec/log_spec.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 122 2 | local log = require 'tarantool-lsp.log' 3 | 4 | describe("log.fmt", function() 5 | it("handles %_", function() 6 | assert.equal("strong", 7 | log.fmt("%_", "strong")) 8 | 9 | assert.equal("nil", 10 | log.fmt("%_", nil)) 11 | 12 | local t1, t2 = {}, {} 13 | assert.equal(tostring(t1) .. " " .. tostring(t2), 14 | log.fmt("%_ %_", t1, t2)) 15 | end) 16 | it("handles %t", function() 17 | assert.equal( 18 | '{ "a", "b", "c" }', 19 | log.fmt("%t", {"a", "b", "c"})) 20 | 21 | assert.equal( 22 | '{ 1,\n = {}\n} { 2 }', 23 | log.fmt("%t %t", setmetatable({1}, {}), {2})) 24 | 25 | local totable = {totable = function() 26 | return {13} 27 | end} 28 | assert.equal( 29 | '12 { 13 }', 30 | log.fmt("%t %t", 12, totable)) 31 | end) 32 | it("handles numeric args", function() 33 | assert.equal("12 nil", 34 | log.fmt("%2$d %1$_", nil, 12)) 35 | 36 | local t = {"okay"} 37 | assert.equal('{ "okay" } '..tostring(t), 38 | log.fmt("%1$t %1$_", t)) 39 | end) 40 | it("handles functions", function() 41 | assert.equal("12 nil", 42 | log.fmt(function() 43 | return "%2$d %1$_", nil, 12 44 | end)) 45 | end) 46 | end) 47 | 48 | describe("log levels", function() 49 | it("can disable logging", function() 50 | log.setTraceLevel("off") 51 | log.file = {write = function() end} 52 | io.write = log.file.write 53 | stub(log.file, "write") 54 | 55 | log("a") 56 | log.debug("b") 57 | log.info("c") 58 | log.warning("d") 59 | log.error("e") 60 | 61 | assert.stub(log.file.write).was_not.called() 62 | end) 63 | 64 | it("can only print important messages", function() 65 | log.setTraceLevel("messages") 66 | log.file = {write = function() end} 67 | io.write = log.file.write 68 | stub(log.file, "write") 69 | 70 | log("a") 71 | assert.stub(log.file.write).was_not.called() 72 | 73 | log.debug("b") 74 | assert.stub(log.file.write).was_not.called() 75 | 76 | log.info("c") 77 | assert.stub(log.file.write).was.called() 78 | log.file.write:clear() 79 | 80 | log.warning("d") 81 | assert.stub(log.file.write).was.called() 82 | log.file.write:clear() 83 | 84 | log.error("e") 85 | assert.stub(log.file.write).was.called() 86 | log.file.write:clear() 87 | end) 88 | 89 | it("can print all messages", function() 90 | log.setTraceLevel("verbose") 91 | log.file = {write = function() end} 92 | io.write = log.file.write 93 | stub(log.file, "write") 94 | 95 | log("a") 96 | log.debug("b") 97 | log.info("c") 98 | log.warning("d") 99 | log.error("e") 100 | 101 | assert.stub(log.file.write).was.called(5) 102 | end) 103 | 104 | it("can fatal error", function() 105 | log.setTraceLevel("verbose") 106 | log.file = {write = function() end} 107 | io.write = log.file.write 108 | stub(log.file, "write") 109 | 110 | assert.has_error(function() 111 | log.fatal("e") 112 | end) 113 | 114 | assert.stub(log.file.write).was.called(1) 115 | end) 116 | end) 117 | -------------------------------------------------------------------------------- /spec/luacheck_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | 3 | describe("textDocument/diagnostics", function() 4 | it("lists local variables", function() 5 | mock_loop(function(rpc) 6 | local text = "local john, jermaine, jack\n" 7 | local doc = { 8 | uri = "file:///tmp/fake.lua" 9 | } 10 | rpc.notify("textDocument/didOpen", { 11 | textDocument = {uri = doc.uri, text = text} 12 | }) 13 | end) 14 | end) 15 | end) 16 | -------------------------------------------------------------------------------- /spec/mock_loop.lua: -------------------------------------------------------------------------------- 1 | 2 | return function(fn, builtins) 3 | local unpack = table.unpack or unpack 4 | _G.Types = {} 5 | _G.Documents = {} 6 | _G.Globals = nil 7 | _G.Config = { 8 | packagePath={"/tmp/?.lua"}, 9 | builtins = builtins or {}, 10 | language = "5.1" 11 | } 12 | _G.Shutdown = false 13 | _G.Initialized = false 14 | 15 | local s_rpc = {} 16 | package.loaded['tarantool-lsp.rpc'] = s_rpc 17 | function s_rpc.respond(id, result) 18 | Args = {{id=id, result = result}} 19 | end 20 | function s_rpc.respondError(id, errorMsg, _, _) 21 | error(string.format("respondError %s: %s ", id, errorMsg)) 22 | end 23 | function s_rpc.notify() 24 | --error(string.format("%s")) 25 | -- throw away notifications: TODO: add busted watcher 26 | --error() 27 | end 28 | function s_rpc.request() 29 | error() 30 | end 31 | function s_rpc.finish() 32 | end 33 | local method_handlers = require 'tarantool-lsp.methods' 34 | 35 | local c_rpc = {next_id = 0} 36 | function c_rpc.respond(_, _) 37 | error() 38 | end 39 | function c_rpc.respondError() 40 | error("unhandled error") 41 | end 42 | function c_rpc.notify(method, params) 43 | coroutine.yield({method = method, params = params}) 44 | end 45 | function c_rpc.request(method, params, req_fn) 46 | local req = coroutine.yield({method = method, params = params, id = c_rpc.next_id}) 47 | assert(c_rpc.next_id == req.id) 48 | assert(req.error == nil) 49 | req_fn(req.result) 50 | c_rpc.next_id = c_rpc.next_id + 1 51 | end 52 | local co = coroutine.create(function() 53 | c_rpc.request("initialize", { 54 | rootPath = "/", 55 | --trace = "off", 56 | trace = "verbose", 57 | }, function() end) 58 | return fn(c_rpc, s_rpc) 59 | end) 60 | _G.Args = {} 61 | 62 | local config = { 63 | language = "5.1", 64 | builtins = {"5_1"}, 65 | packagePath = {"./?.lua"}, 66 | debugMode = false, 67 | documents = {}, 68 | types = {}, 69 | completion_root = require('fio').pathjoin(debug.getinfo(1).source:match("@?(.*/)"), '..', 'test', 'completions'), 70 | _useNativeLuacheck = false -- underscore means "experimental" here 71 | } 72 | 73 | while not Shutdown and coroutine.status(co) ~= 'dead' do 74 | local ok, data = coroutine.resume(co, unpack(Args)) 75 | if not ok then 76 | error("\n"..tostring(data)) 77 | end 78 | if data == nil then -- end 79 | Shutdown = true 80 | elseif data.method then 81 | -- request 82 | assert(method_handlers[data.method], "no method "..data.method) 83 | method_handlers[data.method](config, data.params, data.id) 84 | elseif data.result then 85 | s_rpc.finish(data) 86 | elseif data.error then 87 | error() 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/parser_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | describe("lua-parser in mock loop", function() 3 | it("handles the empty string", function() 4 | mock_loop(function(rpc, s_rpc) 5 | spy.on(s_rpc, "notify") 6 | local text = "" 7 | rpc.notify("textDocument/didOpen", { 8 | textDocument = { 9 | uri = "file:///tmp/fake.lua", 10 | text = text 11 | } 12 | }) 13 | assert.spy(s_rpc.notify).was_called_with( 14 | "textDocument/publishDiagnostics", { 15 | diagnostics = {}, 16 | uri = "file:///tmp/fake.lua" 17 | }) 18 | end) 19 | end) 20 | it("handles functions", function() 21 | mock_loop(function(rpc, s_rpc) 22 | spy.on(s_rpc, "notify") 23 | local text = "function my_fn(a, b, c) return 'str' end" 24 | rpc.notify("textDocument/didOpen", { 25 | textDocument = { 26 | uri = "file:///tmp/fake.lua", 27 | text = text 28 | } 29 | }) 30 | end) 31 | end) 32 | it("errors on functions with extra ids", function() 33 | mock_loop(function(rpc, s_rpc) 34 | spy.on(s_rpc, "notify") 35 | local text = "function my_fn(a, b, c)\ 36 | what return 'str' end\ 37 | function a() lol_wtf() end" 38 | rpc.notify("textDocument/didOpen", { 39 | textDocument = { 40 | uri = "file:///tmp/fake.lua", 41 | text = text 42 | } 43 | }) 44 | end) 45 | end) 46 | end) 47 | 48 | local parser = require 'tarantool-lsp.lua-parser.parser' 49 | 50 | describe("lua-parser version differences:", function() 51 | it("cdata numbers", function() 52 | local body = [[ 53 | local cdata1 = 1LL 54 | local cdata2 = 1ULL 55 | local cdata3 = 0xffULL 56 | local cdata4 = 0xffLL 57 | ]] 58 | assert(parser.parse(body, "out.lua", "luajit")) 59 | for _, v in ipairs{"5.1", "5.2", "5.3"} do 60 | assert.has_errors(function() 61 | assert(parser.parse(body, "out.lua", v)) 62 | end) 63 | end 64 | end) 65 | 66 | it("bitops", function() 67 | local body = [[ 68 | local cdata1 = 1 | 2 69 | local cdata2 = 1 & 2 70 | local cdata3 = 1 ~ 2 71 | local cdata4 = ~ 2 72 | local cdata5 = 1 << 2 73 | local cdata6 = 1 >> 2 74 | ]] 75 | assert(parser.parse(body, "out.lua", "5.3")) 76 | for _, v in ipairs{"5.1", "5.2", "luajit"} do 77 | assert.has_errors(function() 78 | assert(parser.parse(body, "out.lua", v)) 79 | end) 80 | end 81 | end) 82 | 83 | it("floor div", function() 84 | local body = [[ 85 | local cdata1 = 1 // 2 86 | ]] 87 | assert(parser.parse(body, "out.lua", "5.3")) 88 | for _, v in ipairs{"5.1", "5.2", "luajit"} do 89 | assert.has_errors(function() 90 | assert(parser.parse(body, "out.lua", v)) 91 | end) 92 | end 93 | end) 94 | 95 | it("goto/label", function() 96 | local body = [[ 97 | goto mylabel 98 | 99 | ::mylabel:: 100 | ]] 101 | 102 | assert.has_errors(function() 103 | assert(parser.parse(body, "out.lua", "5.1")) 104 | end) 105 | 106 | for _, v in ipairs{"5.2", "5.3", "luajit"} do 107 | assert(parser.parse(body, "out.lua", v)) 108 | end 109 | end) 110 | 111 | pending("locale/unicode", function() 112 | end) 113 | pending("string escape sequences", function() 114 | end) 115 | pending("imaginary numbers", function() 116 | end) 117 | end) 118 | -------------------------------------------------------------------------------- /spec/symbols_spec.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require 'spec.mock_loop' 2 | 3 | describe("textDocument/documentSymbol", function() 4 | it("lists local variables", function() 5 | mock_loop(function(rpc) 6 | local text = "local john, jermaine, jack" 7 | local doc = { 8 | uri = "file:///tmp/fake.lua" 9 | } 10 | rpc.notify("textDocument/didOpen", { 11 | textDocument = {uri = doc.uri, text = text} 12 | }) 13 | rpc.request("textDocument/documentSymbol", { 14 | textDocument = doc, 15 | }, function(out) 16 | local set = {} 17 | for _, o in ipairs(out) do set[o.name] = o.kind end 18 | assert.same({ 19 | jack = 13, 20 | john = 13, 21 | jermaine = 13, 22 | }, set) 23 | for _, o in ipairs(out) do 24 | local r = o.location.range 25 | assert.equal(o.name, r.start and o.name) 26 | set[o.name] = {r.start.line, r.start.character} 27 | end 28 | assert.same({ 29 | john = {0, 6}, 30 | jermaine = {0, 12}, 31 | jack = {0, 22}, 32 | }, set) 33 | end) 34 | end) 35 | end) 36 | it("lists functions", function() 37 | mock_loop(function(rpc) 38 | local text = "local function gem() end\ 39 | local function jam() end\n" 40 | local doc = { 41 | uri = "file:///tmp/fake.lua" 42 | } 43 | rpc.notify("textDocument/didOpen", { 44 | textDocument = {uri = doc.uri, text = text} 45 | }) 46 | rpc.request("textDocument/documentSymbol", { 47 | textDocument = doc, 48 | }, function(out) 49 | local set = {} 50 | for _, o in ipairs(out) do set[o.name] = o.kind end 51 | assert.same({ 52 | gem = 13, 53 | jam = 13, 54 | }, set) 55 | for _, o in ipairs(out) do 56 | local r = o.location.range 57 | assert.equal(o.name, r.start and o.name) 58 | set[o.name] = {r.start.line, r.start.character} 59 | end 60 | assert.same({ 61 | gem = {0, 15}, 62 | jam = {1, 18}, 63 | }, set) 64 | end) 65 | end) 66 | end) 67 | it("lists globals", function() 68 | mock_loop(function(rpc) 69 | local text = "gruber = 1 woah = 2 function yeah() end\n" 70 | local doc = { 71 | uri = "file:///tmp/fake.lua" 72 | } 73 | rpc.notify("textDocument/didOpen", { 74 | textDocument = {uri = doc.uri, text = text} 75 | }) 76 | rpc.request("textDocument/documentSymbol", { 77 | textDocument = doc, 78 | }, function(out) 79 | assert.not_same({}, out) 80 | local set = {} 81 | for _, o in ipairs(out) do set[o.name] = o.kind end 82 | assert.same({ 83 | gruber = 13, 84 | woah = 13, 85 | yeah = 13, 86 | }, set) 87 | for _, o in ipairs(out) do 88 | local r = o.location.range 89 | assert.equal(o.name, r.start and o.name) 90 | set[o.name] = {r.start.line, r.start.character} 91 | end 92 | assert.same({ 93 | gruber = {0, 0}, 94 | woah = {0, 11}, 95 | yeah = {0, 29}, 96 | }, set) 97 | end) 98 | end) 99 | end) 100 | end) 101 | -------------------------------------------------------------------------------- /spec/test-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "test" 2 | version = "scm-0" 3 | source = { 4 | url = "file://" 5 | } 6 | dependencies = { 7 | "luaunit >= 0.2.2-1", 8 | -- "luacheck", 9 | -- "luacov", 10 | -- "luacov-coveralls", 11 | } 12 | build = { 13 | type = "none" 14 | } 15 | -------------------------------------------------------------------------------- /spec/unicode_spec.lua: -------------------------------------------------------------------------------- 1 | local unicode = require 'tarantool-lsp.unicode' 2 | 3 | local unpack = unpack or table.unpack 4 | 5 | local function call(fn, ...) 6 | local args = {...} 7 | return function() 8 | return fn(unpack(args)) 9 | end 10 | end 11 | 12 | describe("utf8 bytes <-> utf16 code units", function() 13 | it("handles ascii", function() 14 | assert.equal(0, unicode.to_codeunits("jeff", 1)) 15 | assert.equal(1, unicode.to_bytes("jeff", 0)) 16 | 17 | assert.equal(1, unicode.to_codeunits("jeff", 2)) 18 | assert.equal(5, unicode.to_bytes("jeff", 4)) 19 | 20 | assert.equal(4, unicode.to_codeunits("jeff", 5)) 21 | assert.equal(2, unicode.to_bytes("jeff", 1)) 22 | 23 | assert.equal(1, unicode.to_codeunits("\tjeff", 2)) 24 | assert.equal(2, unicode.to_bytes("\tjeff", 1)) 25 | 26 | assert.has.errors(call(unicode.to_codeunits, "jeff", 100), "invalid index") 27 | assert.has.errors(call(unicode.to_bytes, "jeff", 100), "invalid index") 28 | 29 | assert.has.errors(call(unicode.to_codeunits, "jeff", 6), "invalid index") 30 | assert.has.errors(call(unicode.to_bytes, "jeff", 5), "invalid index") 31 | 32 | assert.equal(0, unicode.to_codeunits("", 1)) 33 | assert.equal(1, unicode.to_bytes("", 0)) 34 | end) 35 | it("barfs on invalid codepoints", function() 36 | assert.has_no.errors(call(unicode.to_codeunits, " �", 2), "invalid index") 37 | assert.has.errors(call(unicode.to_codeunits, " � ", 3), "invalid index") 38 | end) 39 | it("handles common languages", function() 40 | -- http://www.cl.cam.ac.uk/~mgk25/ucs/examples/quickbrown.txt 41 | local s 42 | 43 | s = "Quizdeltagerne spiste jordbær med fløde,".. 44 | " mens cirkusklovnen Wolther spillede på xylofon." 45 | assert.equal(85, unicode.to_codeunits(s, 89)) 46 | assert.equal(89, unicode.to_bytes(s, 85)) 47 | 48 | s = "Heizölrückstoßabdämpfung" 49 | assert.equal(23, unicode.to_codeunits(s, 28)) 50 | assert.equal(28, unicode.to_bytes(s, 23)) 51 | 52 | s = "Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο" 53 | assert.equal(51, unicode.to_codeunits(s, 102)) 54 | assert.equal(102, unicode.to_bytes(s, 51)) 55 | 56 | -- NOTE: it seems like neovim might interpret this incorrectly 57 | s = "いろはにほへとちりぬるを" 58 | assert.equal(34, unicode.to_bytes(s, 11)) 59 | assert.equal(11, unicode.to_codeunits(s, 34)) 60 | 61 | s = "🤔🤔🤔🤔" 62 | assert.equal(9, unicode.to_bytes(s, 4)) 63 | assert.equal(4, unicode.to_codeunits(s, 9)) 64 | 65 | s = "ℤ is the set of integers" 66 | assert.equal(6, unicode.to_bytes(s, 3)) 67 | assert.equal(3, unicode.to_codeunits(s, 6)) 68 | end) 69 | end) 70 | -------------------------------------------------------------------------------- /tarantool-lsp-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tarantool-lsp" 2 | version = "scm-1" 3 | source = { 4 | url = "git+https://github.com/tarantool/lua-lsp" 5 | } 6 | description = { 7 | homepage = "https://github.com/tarantool/lua-lsp", 8 | license = "MIT" 9 | } 10 | dependencies = { 11 | "lua >= 5.1", 12 | "tarantool", 13 | "checks == 3.0.1-1", 14 | "websocket ~> 0.0.2-1", 15 | "lpeglabel == 1.5.0-1", 16 | } 17 | build = { 18 | type = 'make', 19 | build_target = 'all', 20 | install = { 21 | lua = { 22 | ["tarantool-lsp.analyze"] = "tarantool-lsp/analyze.lua", 23 | ["tarantool-lsp.formatting"] = "tarantool-lsp/formatting.lua", 24 | ["tarantool-lsp.log"] = "tarantool-lsp/log.lua", 25 | ["tarantool-lsp.loop"] = "tarantool-lsp/loop.lua", 26 | ["tarantool-lsp.io-loop"] = "tarantool-lsp/io-loop.lua", 27 | ["tarantool-lsp.lua-parser.parser"] = "tarantool-lsp/lua-parser/parser.lua", 28 | ["tarantool-lsp.lua-parser.scope"] = "tarantool-lsp/lua-parser/scope.lua", 29 | ["tarantool-lsp.lua-parser.validator"] = "tarantool-lsp/lua-parser/validator.lua", 30 | ["tarantool-lsp.tnt-doc.completion-generator"] = "tarantool-lsp/tnt-doc/completion-generator.lua", 31 | ["tarantool-lsp.tnt-doc.doc-manager"] = "tarantool-lsp/tnt-doc/doc-manager.lua", 32 | ["tarantool-lsp.tnt-doc.doc-parser"] = "tarantool-lsp/tnt-doc/doc-parser.lua", 33 | ["tarantool-lsp.methods"] = "tarantool-lsp/methods.lua", 34 | ["tarantool-lsp.rpc"] = "tarantool-lsp/rpc.lua", 35 | ["tarantool-lsp.websocket"] = "tarantool-lsp/websocket.lua", 36 | ["tarantool-lsp.websocket-lib"] = "tarantool-lsp/websocket-lib.lua", 37 | ["tarantool-lsp.unicode"] = "tarantool-lsp/unicode.lua", 38 | ["tarantool-lsp.utils"] = "tarantool-lsp/utils.lua", 39 | ["tarantool-lsp.inspect"] = "tarantool-lsp/inspect.lua", 40 | ["tarantool-lsp.data.5_1"] = "tarantool-lsp/data/5_1.lua", 41 | ["tarantool-lsp"] = "tarantool-lsp.lua" 42 | } 43 | }, 44 | build_variables = { 45 | version = 'scm-1', 46 | }, 47 | install_variables = { 48 | -- Installs lua module: 49 | -- ['cluster.front-bundle'] 50 | INST_LUADIR="$(LUADIR)", 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /tarantool-lsp.lua: -------------------------------------------------------------------------------- 1 | local lsp_websocket = require('tarantool-lsp.websocket-lib') 2 | 3 | return { 4 | create_websocket_handler = lsp_websocket.create_handler, 5 | } 6 | -------------------------------------------------------------------------------- /tarantool-lsp/analyze.lua: -------------------------------------------------------------------------------- 1 | -- Analysis engine 2 | local parser = require 'tarantool-lsp.lua-parser.parser' 3 | local log = require 'tarantool-lsp.log' 4 | local rpc = require 'tarantool-lsp.rpc' 5 | local json = require 'json' 6 | local ok, luacheck = pcall(require, 'luacheck') 7 | if not ok then luacheck = nil end 8 | 9 | local docs = require('tarantool-lsp.tnt-doc.doc-manager') 10 | 11 | local analyze = {} 12 | 13 | local TOPLEVEL = {} 14 | --- turn a luacomplete type into a lsp value 15 | local function translate_luacomplete(into, data) 16 | local function visit_field(key, value, scope) 17 | local Id = {tag = "Id", key, pos=0, posEnd=0, file = "__NONE__"} 18 | if value.type == "table" then 19 | local fields 20 | if key == TOPLEVEL then 21 | fields = scope 22 | else 23 | fields = {} 24 | scope[key] = {Id, { 25 | tag = "Table", 26 | description = value.description, 27 | scope = fields 28 | }} 29 | end 30 | for k, v in pairs(value.fields) do 31 | visit_field(k, v, fields) 32 | end 33 | if value.metatable then 34 | visit_field(key, value.metatable.fields.__index, scope) 35 | end 36 | elseif value.type == "string" then 37 | scope[key] = {Id, { 38 | tag = "String", 39 | description = value.description, 40 | link = value.link 41 | } 42 | } 43 | elseif value.type == "function" then 44 | local variants = nil 45 | if value.variants then 46 | variants = {} 47 | for _, v in ipairs(value.variants) do 48 | table.insert(variants, { 49 | arguments = v.args, 50 | description = v.description, 51 | returns = v.returnTypes 52 | }) 53 | end 54 | end 55 | scope[key] = {Id, { 56 | tag = "Function", 57 | description = value.description, 58 | detail = value.detail, 59 | arguments = value.args or {}, 60 | signature = value.argsDisplay, 61 | returns = value.returnTypes, 62 | variants = variants 63 | } 64 | } 65 | end 66 | end 67 | 68 | visit_field(TOPLEVEL, data, into) 69 | end 70 | 71 | local function set(...) 72 | local t = {} 73 | for i=1, select('#', ...) do 74 | t[select(i, ...)] = true 75 | end 76 | return t 77 | end 78 | 79 | local GLOBAL_SCOPE = 1 80 | local FILE_SCOPE = 2 81 | 82 | local function gen_scopes(config, len, ast, uri) 83 | if not config.globals then 84 | -- FIXME: we need to teach the rest of the system that it's okay for a 85 | -- scopes to not have positions 86 | config.globals = setmetatable({},{ 87 | id=GLOBAL_SCOPE, pos=0, posEnd=math.huge, origin="global" 88 | }) 89 | config.globals._G = {{ 90 | "_G", 91 | tag = "Id", 92 | pos = 0, 93 | posEnd = 0, 94 | file = "__NONE__", 95 | global = true, 96 | }, { 97 | tag = "Table", 98 | pos = 0, 99 | posEnd = 0, 100 | scope = config.globals, 101 | }} 102 | 103 | local info = require('tarantool-lsp.data.5_1') 104 | if info.global then 105 | translate_luacomplete(config.globals, info.global) 106 | end 107 | 108 | if config.complete and config.complete.global then 109 | translate_luacomplete(config.globals, config.complete.global) 110 | end 111 | end 112 | 113 | 114 | local scopes = { 115 | config.globals, 116 | setmetatable({},{ 117 | __index = config.globals, id=FILE_SCOPE, 118 | pos=0, posEnd=len+1, origin="file" 119 | }), 120 | } 121 | 122 | local visit_stat 123 | 124 | local function clean_value(value) 125 | if value == nil then 126 | return {tag = "None"} 127 | end 128 | 129 | local literals = set("Number", "String", "Nil", "True", "False") 130 | if literals[value.tag] then 131 | return { 132 | tag = value.tag == "String" and "String" or "Literal", 133 | pos = value.pos, 134 | posEnd = value.posEnd, 135 | file = uri, 136 | value = value[1] or value.tag 137 | } 138 | elseif value.tag == "Table" then 139 | return { 140 | tag = value.tag, 141 | pos = value.pos, 142 | posEnd = value.posEnd, 143 | file = uri, 144 | scope = value.scope or {}, 145 | } 146 | elseif value.tag == "Call" then 147 | -- find require(), maybe 148 | if value[1].tag == "Id" then 149 | if value[1][1] == "require" and value[2] and value[2].tag == "String" then 150 | return { 151 | tag = "Require", 152 | module = value[2][1] 153 | } 154 | end 155 | 156 | -- FIXME: we need to attach the metatable 157 | -- (which is value[3]) to value[2] 158 | if value[1][1] == "setmetatable" then 159 | return clean_value(value[2]) 160 | end 161 | end 162 | -- otherwise pass call on 163 | return { 164 | tag = value.tag, 165 | pos = value.pos, 166 | posEnd = value.posEnd, 167 | file = uri, 168 | ref = value[1], 169 | _value = value 170 | } 171 | elseif value.tag == "Invoke" then 172 | -- FIXME: untested 173 | return { 174 | tag = value.tag, 175 | pos = value.pos, 176 | posEnd = value.posEnd, 177 | file = uri, 178 | ref = value[1], 179 | _value = value 180 | } 181 | elseif value.tag == "Function" then 182 | return { 183 | tag = value.tag, 184 | pos = value.pos, 185 | posEnd = value.posEnd, 186 | file = uri, 187 | scope = value.scope, 188 | arguments = value[1], 189 | signature = nil 190 | } 191 | elseif value.tag == "Arg" then 192 | return { 193 | tag = value.tag, 194 | pos = value.pos, 195 | posEnd = value.posEnd, 196 | file = uri, 197 | } 198 | elseif value.tag == "Id" then 199 | return { value[1], 200 | tag = value.tag, 201 | pos = value.pos, 202 | posEnd = value.posEnd, 203 | file = uri, 204 | global = value.global, 205 | } 206 | end 207 | --log("unknown obj %t1", value) 208 | return { 209 | tag = "Unknown", 210 | orig = value 211 | } 212 | end 213 | 214 | local function save_local(a, key, value) 215 | if key.tag == "Id" then 216 | assert(type(key[1]) == "string") 217 | assert(key.pos) 218 | 219 | assert(key.posEnd) 220 | 221 | -- This local shadows an existing local in this scope, so we 222 | -- need to create a new scope that represents the current scope 223 | -- post-shadowing 224 | if a[key[1]] then 225 | local a_mt = getmetatable(a) 226 | assert(a_mt.posEnd) 227 | local new_a = setmetatable({}, { 228 | __index = a, 229 | id = #scopes+1, 230 | pos = key.pos, 231 | posEnd = a_mt.posEnd 232 | }) 233 | table.insert(scopes, new_a) 234 | a = new_a 235 | end 236 | a[key[1]] = {key, clean_value(value)} 237 | end 238 | return a 239 | end 240 | 241 | local function is_valid_path(path) 242 | for i, p in ipairs(path) do 243 | if p.tag ~= "String" and not (p.tag == "Id" and i == 1) then 244 | return false 245 | end 246 | end 247 | return true 248 | end 249 | 250 | local function make_path(key) 251 | local a = {} 252 | local function recur(node) 253 | if node.tag == "Index" then 254 | recur(node[1]) 255 | recur(node[2]) 256 | else 257 | table.insert(a, node) 258 | end 259 | end 260 | recur(key) 261 | return a 262 | end 263 | 264 | local function save_pair(scope, key, value) 265 | assert(scope) 266 | assert(key) 267 | assert(key[1]) 268 | assert(value) 269 | scope[key[1]] = {key, clean_value(value)} 270 | end 271 | 272 | local function save_path(a, path, value) 273 | local ia = a 274 | for i=1, #path-1 do 275 | local idx = path[i] 276 | if ia[idx[1]] then 277 | local v = ia[idx[1]][2] 278 | 279 | v.scope = v.scope or {} 280 | ia = v.scope 281 | end 282 | end 283 | local key = path[#path] 284 | save_pair(ia, key, value) 285 | end 286 | 287 | local function save_set(a, key, value) 288 | local k = key[1] 289 | if key.tag == "Index" then 290 | local path = make_path(key) 291 | if is_valid_path(path) then 292 | save_path(a, path, value) 293 | end 294 | return 295 | end 296 | 297 | if a[k] then 298 | -- this makes it much more likely that a local is going to go into 299 | -- unknown states during runtime 300 | -- NOTE: mutating like this means we change the original node 301 | -- instead of creating a new node. this is actually exactly what we 302 | -- want (dirty the old node) but it's counterintuitive 303 | -- we used to pass in nil, but I think now that wrongish or 304 | -- misleading type info is a good start and we can pare it back 305 | -- later considering we still have an unknown/None type 306 | --a[k][2] = clean_value(nil) 307 | a[k][2] = clean_value(value) 308 | else 309 | -- this is a new global var 310 | key.global = true 311 | key.file = uri 312 | scopes[GLOBAL_SCOPE][k] = {key, clean_value(value)} 313 | end 314 | end 315 | 316 | local function save_return(a, return_node) 317 | -- move the return value up to the closest enclosing scope 318 | local mt 319 | repeat 320 | if mt then a = mt.__index end 321 | if a == nil then return end 322 | mt = getmetatable(a) or {} 323 | setmetatable(a, mt) 324 | until mt.origin 325 | mt._return = mt._return or {} 326 | local cleaned_exprs = {} 327 | for _, return_expr in ipairs(return_node) do 328 | table.insert(cleaned_exprs, clean_value(return_expr)) 329 | end 330 | table.insert(mt._return, cleaned_exprs) 331 | end 332 | 333 | local function visit_expr(node, a) 334 | if node.tag == "Function" then 335 | assert(node[2].tag == "Block") 336 | local namelist = node[1] 337 | visit_stat(node[2], a, function(next_a) 338 | getmetatable(next_a).origin = node 339 | node.scope = next_a 340 | for _, name in ipairs(namelist) do 341 | if name.tag ~= "Dots" then 342 | -- when methods are defined like `function a:method()` 343 | -- then self param doesn't include position, presumably 344 | -- because it is implicit. add it back in 345 | if name[1] == "self" and not name.pos then 346 | name.pos = node.pos 347 | name.posEnd = node.posEnd 348 | end 349 | next_a = save_local(next_a, name, { 350 | name, 351 | tag = "Arg", 352 | pos = name.pos, 353 | posEnd = name.posEnd 354 | }) 355 | end 356 | end 357 | return next_a 358 | end) 359 | elseif node.tag == "Call" then 360 | for _, expr in ipairs(node) do 361 | visit_expr(expr, a) 362 | end 363 | elseif node.tag == "Invoke" then 364 | for _, expr in ipairs(node) do 365 | visit_expr(expr, a) 366 | end 367 | elseif node.tag == "Paren" then 368 | visit_expr(node[1], a) 369 | elseif node.tag == "Table" then 370 | node.scope = node.scope or {} 371 | local idx = 1 372 | for _, inode in ipairs(node) do 373 | if inode.tag == "Pair" then 374 | local key, value = inode[1], inode[2] 375 | visit_expr(key, a) 376 | visit_expr(value, a) 377 | save_pair(node.scope, key, value) 378 | else 379 | local key, value = {Tag="Number", idx}, inode 380 | idx = idx + 1 381 | visit_expr(value, a) 382 | save_pair(node.scope, key, value) 383 | end 384 | end 385 | end 386 | end 387 | 388 | function visit_stat(node, a, add_symbols) 389 | assert(node.pos) 390 | assert(node.tag) 391 | if node.tag == "Block" or node.tag == "Do" then 392 | local new_a = setmetatable({}, { 393 | __index = a, 394 | id = #scopes+1, 395 | pos = node.pos, 396 | posEnd = node.posEnd 397 | }) 398 | table.insert(scopes, new_a) 399 | if add_symbols then new_a = add_symbols(new_a) end 400 | for _, i in ipairs(node) do 401 | visit_stat(i, new_a) 402 | end 403 | elseif node.tag == "Set" then 404 | local namelist,exprlist = node[1], node[2] 405 | for i=1, math.max(#namelist, #exprlist) do 406 | local name, expr = namelist[i], exprlist[i] 407 | if expr then 408 | visit_expr(expr, a) 409 | end 410 | 411 | if name then 412 | if expr then 413 | save_set(a, name, expr) 414 | else 415 | -- probably a vararg 416 | save_set(a, name, {tag="Unknown"}) 417 | end 418 | end 419 | end 420 | elseif node.tag == "Return" then 421 | for _, expr in ipairs(node) do 422 | visit_expr(expr, a) 423 | end 424 | save_return(a, node) 425 | elseif node.tag == "Local" then 426 | local namelist,exprlist = node[1], node[2] 427 | if exprlist then 428 | for _, expr in ipairs(exprlist) do 429 | visit_expr(expr, a) 430 | end 431 | end 432 | for i, name in ipairs(namelist) do 433 | a = save_local(a, name, exprlist and exprlist[i]) 434 | end 435 | elseif node.tag == "Localrec" then 436 | local name, expr = node[1][1], node[2][1] 437 | visit_expr(expr, a) 438 | local _ = save_local(a, name, expr) 439 | elseif node.tag == "Fornum" then 440 | for _, n in ipairs(node) do 441 | if n.tag == "Block" then 442 | visit_stat(n, a, function(next_a) 443 | return save_local(next_a, node[1], {tag="Iter"}) 444 | end) 445 | end 446 | end 447 | elseif node.tag == "Forin" then 448 | local namelist, exprlist, block = node[1], node[2], node[3] 449 | for _, expr in ipairs(exprlist) do 450 | visit_expr(expr, a) 451 | end 452 | visit_stat(block, a, function(next_a) 453 | for _, name in ipairs(namelist) do 454 | next_a = save_local(next_a, name, {tag="Iter"}) 455 | end 456 | return next_a 457 | end) 458 | elseif node.tag == "While" then 459 | local expr, block = node[1], node[2] 460 | visit_expr(expr, a) 461 | visit_stat(block, a) 462 | elseif node.tag == "Repeat" then 463 | local block, expr = node[1], node[2] 464 | visit_stat(block, a) 465 | visit_expr(expr, a) 466 | elseif node.tag == "Call" then 467 | for _, expr in ipairs(node) do 468 | visit_expr(expr, a) 469 | end 470 | elseif node.tag == "If" then 471 | for i=1, #node, 2 do 472 | if node[i+1] then 473 | -- if/elseif block 474 | visit_expr(node[i], a) -- test 475 | visit_stat(node[i+1], a) -- body 476 | else 477 | -- else block 478 | visit_stat(node[i], a) 479 | end 480 | end 481 | elseif node.tag == "Comment" then 482 | log.debug("found comment <%d, %d>", node.pos, node.posEnd) 483 | end 484 | end 485 | 486 | visit_stat(ast, scopes[FILE_SCOPE]) 487 | return scopes 488 | end 489 | 490 | local popen_cmd = "sh -c 'cd %q; luacheck %q --filename %q --formatter plain --ranges --codes'" 491 | local message_match = "^([^:]+):(%d+):(%d+)%-(%d+): %(W(%d+)%) (.+)" 492 | local function try_luacheck(config, document) 493 | local diagnostics = {} 494 | local opts = {} 495 | if luacheck and config.root then 496 | local reports 497 | if config._useNativeLuacheck == false then 498 | local tmp_path = "/tmp/check.lua" 499 | local tmp = assert(io.open(tmp_path, "w")) 500 | tmp:write(document.text) 501 | tmp:close() 502 | 503 | local _, ce = document.uri:find(config.root, 1, true) 504 | local fname = document.uri:sub((ce or -1)+2, -1):gsub("file://","") 505 | local root = config.root:gsub("file://", "") 506 | local issues = io.popen(popen_cmd:format(root, tmp_path, fname)) 507 | reports = {{}} 508 | for line in issues:lines() do 509 | local _, l, scol, ecol, code, msg = line:match(message_match) 510 | assert(tonumber(l), line) 511 | assert(tonumber(scol), line) 512 | assert(tonumber(ecol), line) 513 | table.insert(reports[1], { 514 | code = code, 515 | line = tonumber(l), 516 | column = tonumber(scol), 517 | end_column = tonumber(ecol), 518 | message = msg 519 | }) 520 | end 521 | issues:close() 522 | else 523 | reports = luacheck.check_strings({document.text}, {opts}) 524 | end 525 | 526 | for _, issue in ipairs(reports[1]) do 527 | -- FIXME: translate columns to characters 528 | table.insert(diagnostics, { 529 | code = issue.code, 530 | range = { 531 | start = { 532 | line = issue.line-1, 533 | character = issue.column-1 534 | }, 535 | ["end"] = { 536 | line = issue.line-1, 537 | character = issue.end_column 538 | } 539 | }, 540 | -- 1 == error, 2 == warning 541 | severity = issue.code:find("^0") and 1 or 2, 542 | source = "luacheck", 543 | message = issue.message or luacheck.get_message(issue) 544 | }) 545 | end 546 | end 547 | rpc.notify("textDocument/publishDiagnostics", { 548 | uri = document.uri, 549 | diagnostics = diagnostics, 550 | }) 551 | end 552 | 553 | local line_mt = { 554 | __index = function(t, k) 555 | -- line.text generation is lazy because strings are expensive, 556 | -- relatively speaking 557 | if k == "text" then 558 | t.text = t._doc.text:sub(t.start, t["end"]):gsub("\n$", "") 559 | return rawget(t, "text") 560 | end 561 | end 562 | } 563 | 564 | -- stolen from https://rosettacode.org/wiki/Longest_common_prefix#Lua 565 | -- probably not the fastest impl but /shrug 566 | local function lcp(strList) 567 | local shortest = math.huge 568 | for _, str in ipairs(strList) do 569 | if str:len() < shortest then shortest = str:len() end 570 | end 571 | for strPos = 1, shortest do 572 | local first = strList[1]:sub(strPos, strPos) 573 | if not first then return strPos-1 end 574 | for listPos = 2, #strList do 575 | if strList[listPos]:sub(strPos, strPos) ~= first then 576 | return strPos-1 577 | end 578 | end 579 | end 580 | return shortest 581 | end 582 | 583 | function analyze.refresh(config, document) 584 | local text = document.text 585 | 586 | local lines = {} 587 | local ii = 1 588 | local len = text:len() 589 | while ii <= len do 590 | local pos_s, pos_e = string.find(document.text, "([^\n]*)\n?", ii) 591 | table.insert(lines, setmetatable({ 592 | start = pos_s, ["end"] = pos_e, _doc = document 593 | }, line_mt)) 594 | ii = pos_e + 1 595 | end 596 | document.lines = lines 597 | 598 | local start_time = os.clock() 599 | local ast, err = parser.parse(document.text, document.uri, config.language) 600 | if ast then 601 | document.ast = ast 602 | document.validtext = document.text 603 | document.scopes = gen_scopes(config, #document.text, document.ast, document.uri) 604 | try_luacheck(config, document) 605 | else 606 | document.dirty = lcp{document.text, document.validtext} 607 | local line, column = err.line, err.column 608 | assert(err.line) 609 | return rpc.notify("textDocument/publishDiagnostics", { 610 | uri = document.uri, 611 | diagnostics = { { 612 | code = "011", -- this is a luacheck code 613 | range = { 614 | start = {line = line-1, character = column-1}, 615 | -- the parser does not keep track of the end of the error 616 | -- so only pass in what we know 617 | ["end"] = {line = line-1, character = column} 618 | }, 619 | -- 1 == error, 2 == warning 620 | severity = 1, 621 | source = "parser", 622 | message = err.message, 623 | } } 624 | }) 625 | -- FIXME: in this state (aka broken) the position numbers of the old 626 | -- AST are out of sync with the new text object. 627 | end 628 | local path = document.uri 629 | if config.root then 630 | local _, e = string.find(path, config.root, 1, true) 631 | path = string.sub(path, (e or -1)+2, -1) 632 | end 633 | log.verbose("%s: analyze took %f s", path, os.clock() - start_time) 634 | end 635 | 636 | function analyze.document(config, uri) 637 | local ref = nil 638 | if type(uri) == "table" then 639 | ref = uri 640 | uri = uri.uri 641 | end 642 | if config.documents[uri] then 643 | if ref and ref.text then 644 | config.documents[uri].text = ref.text 645 | analyze.refresh(config, config.documents[uri]) 646 | end 647 | return config.documents[uri] 648 | end 649 | local document = ref or {} 650 | document.uri = uri 651 | 652 | if not document.text then 653 | if config.web_server then 654 | document.text = '' 655 | else 656 | local f = assert(io.open(uri:gsub("^[^:]+://", ""), "r")) 657 | document.text = f:read("*a") 658 | f:close() 659 | end 660 | end 661 | 662 | analyze.refresh(config, document) 663 | 664 | config.documents[uri] = document 665 | 666 | 667 | return document 668 | end 669 | 670 | function analyze.module(config, mod) 671 | -- FIXME: load path from config file 672 | mod = mod:gsub("%.", "/") 673 | 674 | local internalLibs = config.libraries 675 | if internalLibs[mod] then 676 | local ok, lib = pcall(require, 'tarantool-lsp.completions.' .. mod) 677 | if ok then 678 | local _scope = {} 679 | translate_luacomplete(_scope, lib) 680 | 681 | local fake_file_scope = setmetatable({}, { 682 | _return = { 683 | [1] = { 684 | [1] = _scope[mod][2] 685 | } 686 | } 687 | }) 688 | local fake_document = { 689 | scopes = { 690 | [1] = nil, -- Ignore Global scope (it's redundant here) 691 | [2] = fake_file_scope 692 | } 693 | } 694 | 695 | return fake_document 696 | else 697 | log.warning("Can't find completions for %s library", mod) 698 | end 699 | end 700 | 701 | for _, template in ipairs(config.packagePath) do 702 | local p = template:gsub("^%./", config.root.."/"):gsub("?", mod) 703 | local uri = "file://"..p 704 | if config.documents[uri] then 705 | return analyze.document(config, uri) 706 | elseif config.documents[uri] ~= false then 707 | local f = io.open(p) 708 | if f then 709 | f:close() 710 | return analyze.document(config, uri) 711 | else 712 | -- cache missing file 713 | config.documents[uri] = false 714 | end 715 | end 716 | end 717 | return nil, "module not found" 718 | end 719 | 720 | local function split_pkg_path(path) 721 | local path_ids = {} 722 | 723 | local i = 1 724 | while path:find(";", i) do 725 | local is, ie = path:find(";", i) 726 | table.insert(path_ids, path:sub(i, is-1)) 727 | i = ie+1 728 | end 729 | table.insert(path_ids, path:sub(i, -1)) 730 | 731 | for _, s in ipairs(path_ids) do 732 | assert(s:match("?"), "path missing '?': "..s) 733 | end 734 | 735 | return path_ids 736 | end 737 | 738 | local function add_types(config, new_types) 739 | for k, v in pairs(new_types) do 740 | config.types[k] = {tag="Table", scope = {}} 741 | translate_luacomplete(config.types[k].scope, v) 742 | end 743 | end 744 | 745 | --- load a .luacompleterc file into Config, for later use 746 | function analyze.load_completerc(config, root) 747 | local f = io.open(root.."/.luacompleterc") 748 | if f then 749 | local s = assert(f:read("*a")) 750 | local data, err = json.decode(s) 751 | if data then 752 | config.complete = data 753 | if data.namedTypes then 754 | add_types(config, data.namedTypes) 755 | end 756 | 757 | if data.luaVersion == "love" then 758 | config.builtins = {"love-completions", "luajit-2_0"} 759 | config.language = "luajit" 760 | elseif data.luaVersion then 761 | config.builtins = {(data.luaVersion:gsub("%.","_"))} 762 | config.language = data.luaVersion 763 | if config.language:match("luajit") then 764 | config.language = "luajit" 765 | end 766 | end 767 | 768 | for _, builtin in ipairs(config.builtins) do 769 | local info = require('lua-lsp.data.'..builtin) 770 | if info.namedTypes then 771 | add_types(config, info.namedTypes) 772 | end 773 | end 774 | 775 | if data.packagePath then 776 | assert(type(data.packagePath) == "string") 777 | config.packagePath = split_pkg_path(data.packagePath) 778 | end 779 | 780 | if data.cwd then 781 | log.error("field 'cwd' in .luacompleterc is not supported'") 782 | end 783 | else 784 | log.warning(".luacompleterc: %s", tostring(err)) 785 | end 786 | end 787 | end 788 | 789 | --- Create table from vararg, skipping nil values 790 | local function skip(...) 791 | local t = {} 792 | for i=1, select('#', ...) do 793 | -- nil values won't increment length 794 | t[#t+1] = select(i, ...) 795 | end 796 | return t 797 | end 798 | 799 | --- Load a .luacheckrc into Config for later use 800 | function analyze.load_luacheckrc(config, root) 801 | if luacheck then 802 | local cfg = require 'luacheck.config' 803 | -- stack_configs is not in release builds of luacheck yet 804 | if cfg.stack_configs then 805 | local default = cfg.load_config() 806 | local current = cfg.load_config(root.."/.luacheckrc") 807 | config.luacheckrc = cfg.stack_configs(skip(default, current)) 808 | end 809 | end 810 | end 811 | 812 | return analyze 813 | -------------------------------------------------------------------------------- /tarantool-lsp/current-tag-doc: -------------------------------------------------------------------------------- 1 | 2.2 2 | 3 | -------------------------------------------------------------------------------- /tarantool-lsp/formatting.lua: -------------------------------------------------------------------------------- 1 | --- This wraps over the existing lua code formatters. There's no actively 2 | -- maintained solution that solves everyone's problems, unfortunately. 3 | 4 | local log = require 'tarantool-lsp.log' 5 | 6 | local drivers = {} 7 | 8 | -- https://luarocks.org/modules/luarocks/formatter 9 | drivers['formatter'] = function(formatter) 10 | return { 11 | format = function(text, opts) 12 | return formatter.indentcode(text, "\n", true, opts.indent) 13 | end, 14 | driver = "Formatter" 15 | } 16 | end 17 | 18 | -- https://github.com/martin-eden/lua_code_formatter 19 | drivers['lcf.workshop.base'] = function() 20 | -- luacheck: globals request 21 | local get_ast = request('!.lua.code.get_ast') 22 | local get_formatted_code = request('!.lua.code.ast_as_code') 23 | return { 24 | format = function(text, opts) 25 | return get_formatted_code(get_ast(text), { 26 | indent_chunk = opts.indent, 27 | }) 28 | end, 29 | driver = "lcf" 30 | } 31 | end 32 | 33 | -- No-op 34 | local noDriver = { 35 | format = function(text) 36 | log.warning("No formatter installed!") 37 | return text 38 | end, 39 | driver = "noop" 40 | } 41 | 42 | for mod, driver in pairs(drivers) do 43 | local ok, m_or_err = pcall(require, mod) 44 | if ok then 45 | return driver(m_or_err) 46 | end 47 | end 48 | 49 | return noDriver 50 | -------------------------------------------------------------------------------- /tarantool-lsp/inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | local function rawpairs(t) 37 | return next, t, nil 38 | end 39 | 40 | -- Apostrophizes the string if it has quotes, but not aphostrophes 41 | -- Otherwise, it returns a regular quoted string 42 | local function smartQuote(str) 43 | if str:match('"') and not str:match("'") then 44 | return "'" .. str .. "'" 45 | end 46 | return '"' .. str:gsub('"', '\\"') .. '"' 47 | end 48 | 49 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 50 | local shortControlCharEscapes = { 51 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 52 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 53 | } 54 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 55 | for i=0, 31 do 56 | local ch = string.char(i) 57 | if not shortControlCharEscapes[ch] then 58 | shortControlCharEscapes[ch] = "\\"..i 59 | longControlCharEscapes[ch] = string.format("\\%03d", i) 60 | end 61 | end 62 | 63 | local function escape(str) 64 | return (str:gsub("\\", "\\\\") 65 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 66 | :gsub("%c", shortControlCharEscapes)) 67 | end 68 | 69 | local function isIdentifier(str) 70 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 71 | end 72 | 73 | local function isSequenceKey(k, sequenceLength) 74 | return type(k) == 'number' 75 | and 1 <= k 76 | and k <= sequenceLength 77 | and math.floor(k) == k 78 | end 79 | 80 | local defaultTypeOrders = { 81 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 82 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 83 | } 84 | 85 | local function sortKeys(a, b) 86 | local ta, tb = type(a), type(b) 87 | 88 | -- strings and numbers are sorted numerically/alphabetically 89 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 90 | 91 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 92 | -- Two default types are compared according to the defaultTypeOrders table 93 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 94 | elseif dta then return true -- default types before custom ones 95 | elseif dtb then return false -- custom types after default ones 96 | end 97 | 98 | -- custom types are sorted out alphabetically 99 | return ta < tb 100 | end 101 | 102 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 103 | -- tables aren't pure sequences. So we implement our own # operator. 104 | local function getSequenceLength(t) 105 | local len = 1 106 | local v = rawget(t,len) 107 | while v ~= nil do 108 | len = len + 1 109 | v = rawget(t,len) 110 | end 111 | return len - 1 112 | end 113 | 114 | local function getNonSequentialKeys(t) 115 | local keys, keysLength = {}, 0 116 | local sequenceLength = getSequenceLength(t) 117 | for k,_ in rawpairs(t) do 118 | if not isSequenceKey(k, sequenceLength) then 119 | keysLength = keysLength + 1 120 | keys[keysLength] = k 121 | end 122 | end 123 | table.sort(keys, sortKeys) 124 | return keys, keysLength, sequenceLength 125 | end 126 | 127 | local function countTableAppearances(t, tableAppearances) 128 | tableAppearances = tableAppearances or {} 129 | 130 | if type(t) == 'table' then 131 | if not tableAppearances[t] then 132 | tableAppearances[t] = 1 133 | for k,v in rawpairs(t) do 134 | countTableAppearances(k, tableAppearances) 135 | countTableAppearances(v, tableAppearances) 136 | end 137 | countTableAppearances(getmetatable(t), tableAppearances) 138 | else 139 | tableAppearances[t] = tableAppearances[t] + 1 140 | end 141 | end 142 | 143 | return tableAppearances 144 | end 145 | 146 | local copySequence = function(s) 147 | local copy, len = {}, #s 148 | for i=1, len do copy[i] = s[i] end 149 | return copy, len 150 | end 151 | 152 | local function makePath(path, ...) 153 | local keys = {...} 154 | local newPath, len = copySequence(path) 155 | for i=1, #keys do 156 | newPath[len + i] = keys[i] 157 | end 158 | return newPath 159 | end 160 | 161 | local function processRecursive(process, item, path, visited) 162 | if item == nil then return nil end 163 | if visited[item] then return visited[item] end 164 | 165 | local processed = process(item, path) 166 | if type(processed) == 'table' then 167 | local processedCopy = {} 168 | visited[item] = processedCopy 169 | local processedKey 170 | 171 | for k,v in rawpairs(processed) do 172 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 173 | if processedKey ~= nil then 174 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 175 | end 176 | end 177 | 178 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 179 | if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field 180 | setmetatable(processedCopy, mt) 181 | processed = processedCopy 182 | end 183 | return processed 184 | end 185 | 186 | 187 | 188 | ------------------------------------------------------------------- 189 | 190 | local Inspector = {} 191 | local Inspector_mt = {__index = Inspector} 192 | 193 | function Inspector:puts(...) 194 | local args = {...} 195 | local buffer = self.buffer 196 | local len = #buffer 197 | for i=1, #args do 198 | len = len + 1 199 | buffer[len] = args[i] 200 | end 201 | end 202 | 203 | function Inspector:down(f) 204 | self.level = self.level + 1 205 | f() 206 | self.level = self.level - 1 207 | end 208 | 209 | function Inspector:tabify() 210 | self:puts(self.newline, string.rep(self.indent, self.level)) 211 | end 212 | 213 | function Inspector:alreadyVisited(v) 214 | return self.ids[v] ~= nil 215 | end 216 | 217 | function Inspector:getId(v) 218 | local id = self.ids[v] 219 | if not id then 220 | local tv = type(v) 221 | id = (self.maxIds[tv] or 0) + 1 222 | self.maxIds[tv] = id 223 | self.ids[v] = id 224 | end 225 | return tostring(id) 226 | end 227 | 228 | function Inspector:putKey(k) 229 | if isIdentifier(k) then return self:puts(k) end 230 | self:puts("[") 231 | self:putValue(k) 232 | self:puts("]") 233 | end 234 | 235 | function Inspector:putTable(t) 236 | if t == inspect.KEY or t == inspect.METATABLE then 237 | self:puts(tostring(t)) 238 | elseif self:alreadyVisited(t) then 239 | self:puts('
') 240 | elseif self.level >= self.depth then 241 | self:puts('{...}') 242 | else 243 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 244 | 245 | local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) 246 | local mt = getmetatable(t) 247 | 248 | self:puts('{') 249 | self:down(function() 250 | local count = 0 251 | for i=1, sequenceLength do 252 | if count > 0 then self:puts(',') end 253 | self:puts(' ') 254 | self:putValue(t[i]) 255 | count = count + 1 256 | end 257 | 258 | for i=1, nonSequentialKeysLength do 259 | local k = nonSequentialKeys[i] 260 | if count > 0 then self:puts(',') end 261 | self:tabify() 262 | self:putKey(k) 263 | self:puts(' = ') 264 | self:putValue(t[k]) 265 | count = count + 1 266 | end 267 | 268 | if type(mt) == 'table' then 269 | if count > 0 then self:puts(',') end 270 | self:tabify() 271 | self:puts(' = ') 272 | self:putValue(mt) 273 | end 274 | end) 275 | 276 | if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } 277 | self:tabify() 278 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 279 | self:puts(' ') 280 | end 281 | 282 | self:puts('}') 283 | end 284 | end 285 | 286 | function Inspector:putValue(v) 287 | local tv = type(v) 288 | 289 | if tv == 'string' then 290 | self:puts(smartQuote(escape(v))) 291 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or 292 | tv == 'cdata' or tv == 'ctype' then 293 | self:puts(tostring(v)) 294 | elseif tv == 'table' then 295 | self:putTable(v) 296 | else 297 | self:puts('<', tv, ' ', self:getId(v), '>') 298 | end 299 | end 300 | 301 | ------------------------------------------------------------------- 302 | 303 | function inspect.inspect(root, options) 304 | options = options or {} 305 | 306 | local depth = options.depth or math.huge 307 | local newline = options.newline or '\n' 308 | local indent = options.indent or ' ' 309 | local process = options.process 310 | 311 | if process then 312 | root = processRecursive(process, root, {}, {}) 313 | end 314 | 315 | local inspector = setmetatable({ 316 | depth = depth, 317 | level = 0, 318 | buffer = {}, 319 | ids = {}, 320 | maxIds = {}, 321 | newline = newline, 322 | indent = indent, 323 | tableAppearances = countTableAppearances(root) 324 | }, Inspector_mt) 325 | 326 | inspector:putValue(root) 327 | 328 | return table.concat(inspector.buffer) 329 | end 330 | 331 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 332 | 333 | return inspect 334 | 335 | -------------------------------------------------------------------------------- /tarantool-lsp/io-loop.lua: -------------------------------------------------------------------------------- 1 | local json = require 'json' 2 | 3 | local io_loop = {} 4 | 5 | local valid_content_type = { 6 | ["application/vscode-jsonrpc; charset=utf-8"] = true, 7 | -- the spec says to be lenient in this case 8 | ["application/vscode-jsonrpc; charset=utf8"] = true 9 | } 10 | 11 | 12 | function io_loop.decode() 13 | local line = io.read("*l") 14 | if line == nil then 15 | return nil, "eof" 16 | end 17 | line = line:gsub("\13", "") 18 | local content_length 19 | while line ~= "" do 20 | local key, val = line:match("^([^:]+): (.+)$") 21 | assert(key, string.format("%q", tostring(line))) 22 | assert(val) 23 | if key == "Content-Length" then 24 | content_length = tonumber(val) 25 | elseif key == "Content-Type" then 26 | assert(valid_content_type[val], "Invalid Content-Type") 27 | else 28 | error("unexpected") 29 | end 30 | line = io.read("*l") 31 | line = line:gsub("\13", "") 32 | end 33 | 34 | -- body 35 | assert(content_length) 36 | local data = io.read(content_length) 37 | data = data:gsub("\13", "") 38 | data = assert(json.decode(data)) 39 | assert(data["jsonrpc"] == "2.0") 40 | return data 41 | end 42 | 43 | return io_loop 44 | -------------------------------------------------------------------------------- /tarantool-lsp/log.lua: -------------------------------------------------------------------------------- 1 | local rpc = require 'tarantool-lsp.rpc' 2 | local inspect = require 'tarantool-lsp.inspect' 3 | 4 | -- Using logger wrapper for LSP server log supports 5 | local tnt_logger = require 'log' 6 | 7 | local log = {} 8 | 9 | log.enabled = { default = true, verbose = true } 10 | 11 | local function pack(...) 12 | return {n = select('#', ...), ...} 13 | end 14 | 15 | function log.fmt(s, ...) 16 | local args 17 | if type(s) == 'function' then 18 | args = pack(s(...)) 19 | s = args[1] 20 | for i = 2, args.n do 21 | args[i-1] = args[i] 22 | if i == args.n then 23 | args[i] = nil 24 | end 25 | end 26 | args.n = args.n - 1 27 | else 28 | args = pack(...) 29 | end 30 | 31 | local i = 0 32 | return (string.gsub(s, "%%[A-Za-z._+$0-9]+", function(input) 33 | i = i + 1 34 | if input:match("^%%%d+%$") then 35 | i, input = input:match("^%%(%d+)%$(.*)$") 36 | i = tonumber(i) 37 | input = "%"..input 38 | end 39 | 40 | if input == "%_" then 41 | return tostring(args[i]) 42 | elseif input:match("^%%t") then 43 | local t = args[i] 44 | local depth = input:match("^%%t(%d*)") 45 | depth = tonumber(depth) 46 | if type(t) ~= 'table' then 47 | return tostring(t) 48 | elseif t.totable then 49 | t = t:totable() 50 | end 51 | return inspect(t, {depth = depth}) 52 | else 53 | return string.format(input, args[i]) 54 | end 55 | end)) 56 | end 57 | 58 | function log.setTraceLevel(trace) 59 | if trace == "off" then 60 | log.enabled = {default = false, verbose = false} 61 | elseif trace == "messages" then 62 | log.enabled = {default = true, verbose = false} 63 | elseif trace == "verbose" then 64 | log.enabled = {default = true, verbose = true} 65 | end 66 | end 67 | 68 | local message_types = { error = 1, warning = 2, info = 3, log = 4 } 69 | function log.info(...) 70 | if log.enabled.default then 71 | local msg = log.fmt(...) 72 | tnt_logger.info(msg) 73 | 74 | rpc.notify("window/logMessage", { 75 | message = msg, 76 | type = message_types.info, 77 | }) 78 | end 79 | end 80 | 81 | function log.warning(...) 82 | if log.enabled.default then 83 | local msg = log.fmt(...) 84 | tnt_logger.warn(msg) 85 | 86 | rpc.notify("window/logMessage", { 87 | message = msg, 88 | type = message_types.warning, 89 | }) 90 | end 91 | end 92 | 93 | function log.error(...) 94 | if log.enabled.default then 95 | local msg = log.fmt(...) 96 | tnt_logger.error(msg) 97 | 98 | rpc.notify("window/logMessage", { 99 | message = msg, 100 | type = message_types.error, 101 | }) 102 | end 103 | end 104 | 105 | function log.fatal(...) 106 | local msg = log.fmt(...) 107 | log.error("%s", msg) 108 | error(msg, 2) 109 | end 110 | 111 | function log.verbose(...) 112 | if log.enabled.verbose then 113 | local msg = log.fmt(...) 114 | tnt_logger.verbose(msg) 115 | 116 | rpc.notify("window/logMessage", { 117 | message = msg, 118 | type = message_types.log, 119 | }) 120 | end 121 | end 122 | 123 | function log.debug(...) 124 | if log.enabled.verbose then 125 | local info = debug.getinfo(2, 'lS') 126 | local msg = log.fmt(...) 127 | local pre = string.format( 128 | "%s:%d: ", 129 | info.short_src, 130 | info.currentline) 131 | 132 | tnt_logger.debug(pre, msg, "\n") 133 | 134 | rpc.notify("window/logMessage", { 135 | message = msg, 136 | type = message_types.log, 137 | }) 138 | end 139 | end 140 | log.d = log.debug 141 | 142 | setmetatable(log, { 143 | __call = function(_, ...) 144 | if log.enabled.verbose then 145 | local info = debug.getinfo(2, 'lS') 146 | local msg = log.fmt(...) 147 | local pre = string.format( 148 | "%s:%d: ", 149 | info.short_src, 150 | info.currentline) 151 | 152 | tnt_logger.verbose(pre, msg) 153 | 154 | --rpc.notify("window/logMessage", { 155 | -- message = msg, 156 | -- type = message_types.log, 157 | --}) 158 | end 159 | end 160 | }) 161 | 162 | return log 163 | -------------------------------------------------------------------------------- /tarantool-lsp/loop.lua: -------------------------------------------------------------------------------- 1 | local rpc = require 'tarantool-lsp.rpc' 2 | local io_loop = require 'tarantool-lsp.io-loop' 3 | local log = require 'tarantool-lsp.log' 4 | local method_handlers = require 'tarantool-lsp.methods' 5 | local fio = require 'fio' 6 | 7 | local config = { 8 | language = "5.1", 9 | builtins = {"5_1"}, 10 | packagePath = {"./?.lua"}, 11 | documents = {}, 12 | types = {}, 13 | globals = nil, 14 | debugMode = false, 15 | completion_root = fio.pathjoin(_G._ROOT_PATH, 'tarantool-lsp/completions'), 16 | libraries = {}, 17 | _useNativeLuacheck = false -- underscore means "experimental" here 18 | } 19 | 20 | _G.Types = _G.Types or {} 21 | _G.Documents = _G.Documents or {} 22 | _G.Globals = _G.Globals -- defined in analyze.lua 23 | -- selfish default 24 | 25 | _G.Shutdown = false 26 | _G.Initialized = _G.Initialized or false 27 | _G.print = function() 28 | error("illegal print, use log() instead:", 2) 29 | end 30 | 31 | local function reload_all() 32 | for name, _ in pairs(package.loaded) do 33 | if name:find("^lua%-lsp") and name ~= 'tarantool-lsp.log' then 34 | package.loaded[name] = nil 35 | end 36 | end 37 | log.verbose("===========================") 38 | method_handlers = require 'tarantool-lsp.methods' 39 | end 40 | 41 | local function main(_) 42 | while not Shutdown do 43 | -- header 44 | local data, err = io_loop.decode() 45 | if config.debugMode then 46 | reload_all() 47 | end 48 | 49 | if data == nil then 50 | if err == "eof" then return os.exit(1) end 51 | error(err) 52 | elseif data.method then 53 | -- request 54 | if not method_handlers[data.method] then 55 | -- log.verbose("confused by %t", data) 56 | err = string.format("%q: Not found/NYI", tostring(data.method)) 57 | if data.id then 58 | rpc.respondError(data.id, err, "MethodNotFound") 59 | else 60 | log.warning("%s", err) 61 | end 62 | else 63 | local ok 64 | ok, err = xpcall(function() 65 | local response = method_handlers[data.method](config, data.params, data.id) 66 | if response then 67 | io.write("Content-Length: ".. string.len(response).."\r\n\r\n"..response) 68 | io.flush() 69 | end 70 | end, debug.traceback) 71 | if not ok then 72 | if data.id then 73 | local msgError = rpc.respondError(data.id, err, "InternalError") 74 | io.write("Content-Length: ".. string.len(msgError).."\r\n\r\n"..msgError) 75 | io.flush() 76 | else 77 | log.warning("%s", tostring(err)) 78 | end 79 | end 80 | end 81 | elseif data.result then 82 | rpc.finish(data) 83 | elseif data.error then 84 | log("client error:%s", data.error.message) 85 | end 86 | end 87 | 88 | os.exit(0) 89 | end 90 | 91 | return main 92 | --https://code.visualstudio.com/blogs/2016/06/27/common-language-protocol 93 | -------------------------------------------------------------------------------- /tarantool-lsp/lua-parser/parser.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This module implements a parser for Lua 5.3 with LPeg, 3 | and generates an Abstract Syntax Tree in the Metalua format. 4 | For more information about Metalua, please, visit: 5 | https://github.com/fab13n/metalua-parser 6 | 7 | block: { stat* } 8 | 9 | stat: 10 | `Do{ stat* } 11 | | `Set{ {lhs+} {expr+} } -- lhs1, lhs2... = e1, e2... 12 | | `While{ expr block } -- while e do b end 13 | | `Repeat{ block expr } -- repeat b until e 14 | | `If{ (expr block)+ block? } -- if e1 then b1 [elseif e2 then b2] ... [else bn] end 15 | | `Fornum{ ident expr expr expr? block } -- for ident = e, e[, e] do b end 16 | | `Forin{ {ident+} {expr+} block } -- for i1, i2... in e1, e2... do b end 17 | | `Local{ {ident+} {expr+}? } -- local i1, i2... = e1, e2... 18 | | `Localrec{ ident expr } -- only used for 'local function' 19 | | `Goto{ } -- goto str 20 | | `Label{ } -- ::str:: 21 | | `Return{ } -- return e1, e2... 22 | | `Break -- break 23 | | apply 24 | 25 | expr: 26 | `Nil 27 | | `Dots 28 | | `True 29 | | `False 30 | | `Number{ } 31 | | `String{ } 32 | | `Function{ { `Id{ }* `Dots? } block } 33 | | `Table{ ( `Pair{ expr expr } | expr )* } 34 | | `Op{ opid expr expr? } 35 | | `Paren{ expr } -- significant to cut multiple values returns 36 | | apply 37 | | lhs 38 | 39 | apply: 40 | `Call{ expr expr* } 41 | | `Invoke{ expr `String{ } expr* } 42 | 43 | lhs: `Id{ } | `Index{ expr expr } 44 | 45 | opid: -- includes additional operators from Lua 5.3 46 | 'add' | 'sub' | 'mul' | 'div' 47 | | 'idiv' | 'mod' | 'pow' | 'concat' 48 | | 'band' | 'bor' | 'bxor' | 'shl' | 'shr' 49 | | 'eq' | 'lt' | 'le' | 'and' | 'or' 50 | | 'unm' | 'len' | 'bnot' | 'not' 51 | 52 | ALLOYED'S NOTES: 53 | for IDE use we want a "partial" parser. this can mean either returning the 54 | ast as-is after errors or trying to parser around the error. probably number 55 | 1 is fine. 56 | 57 | pos and posEnd seem pretty goofed up when it comes to Id nodes (which are the 58 | only ones I looked into), should probably be fixed 59 | 60 | We need to support lua 5.2/luajit and 5.1 ideally. Details: 61 | 62 | for 5.2 support: 63 | * no bitops 64 | 65 | for 5.1 support: 66 | * Lua identifiers are locale-defined (AKA current behavior) 67 | * no goto 68 | * no hex escape/star escape 69 | 70 | for luajit support: 71 | http://luajit.org/extensions.html 72 | * identifiers support all utf-8 characters. TODO: find out what that means 73 | * yes goto 74 | * cdata numbers 75 | 76 | ]] 77 | -- luacheck: ignore 78 | 79 | local lpeg = require "lpeglabel" 80 | 81 | local locale = lpeg.locale() 82 | 83 | local P, S, V = lpeg.P, lpeg.S, lpeg.V 84 | local C, Carg, Cb, Cc = lpeg.C, lpeg.Carg, lpeg.Cb, lpeg.Cc 85 | local Cf, Cg, Cmt, Cp, Cs, Ct = lpeg.Cf, lpeg.Cg, lpeg.Cmt, lpeg.Cp, lpeg.Cs, lpeg.Ct 86 | local Lc, T = lpeg.Lc, lpeg.T 87 | 88 | local alpha, digit, alnum = locale.alpha, locale.digit, locale.alnum 89 | local xdigit = locale.xdigit 90 | local space = locale.space 91 | 92 | 93 | -- error message auxiliary functions 94 | 95 | local labels = { 96 | { "ErrExtra", "unexpected character(s), expected EOF" }, 97 | { "ErrInvalidStat", "unexpected token, invalid start of statement" }, 98 | 99 | { "ErrEndIf", "expected 'end' to close the if statement" }, 100 | { "ErrExprIf", "expected a condition after 'if'" }, 101 | { "ErrThenIf", "expected 'then' after the condition" }, 102 | { "ErrExprEIf", "expected a condition after 'elseif'" }, 103 | { "ErrThenEIf", "expected 'then' after the condition" }, 104 | 105 | { "ErrEndDo", "expected 'end' to close the do block" }, 106 | { "ErrExprWhile", "expected a condition after 'while'" }, 107 | { "ErrDoWhile", "expected 'do' after the condition" }, 108 | { "ErrEndWhile", "expected 'end' to close the while loop" }, 109 | { "ErrUntilRep", "expected 'until' at the end of the repeat loop" }, 110 | { "ErrExprRep", "expected a conditions after 'until'" }, 111 | 112 | { "ErrForRange", "expected a numeric or generic range after 'for'" }, 113 | { "ErrEndFor", "expected 'end' to close the for loop" }, 114 | { "ErrExprFor1", "expected a starting expression for the numeric range" }, 115 | { "ErrCommaFor", "expected ',' to split the start and end of the range" }, 116 | { "ErrExprFor2", "expected an ending expression for the numeric range" }, 117 | { "ErrExprFor3", "expected a step expression for the numeric range after ','" }, 118 | { "ErrInFor", "expected '=' or 'in' after the variable(s)" }, 119 | { "ErrEListFor", "expected one or more expressions after 'in'" }, 120 | { "ErrDoFor", "expected 'do' after the range of the for loop" }, 121 | 122 | { "ErrDefLocal", "expected a function definition or assignment after local" }, 123 | { "ErrNameLFunc", "expected a function name after 'function'" }, 124 | { "ErrEListLAssign", "expected one or more expressions after '='" }, 125 | { "ErrEListAssign", "expected one or more expressions after '='" }, 126 | 127 | { "ErrFuncName", "expected a function name after 'function'" }, 128 | { "ErrNameFunc1", "expected a function name after '.'" }, 129 | { "ErrNameFunc2", "expected a method name after ':'" }, 130 | { "ErrOParenPList", "expected '(' for the parameter list" }, 131 | { "ErrCParenPList", "expected ')' to close the parameter list" }, 132 | { "ErrEndFunc", "expected 'end' to close the function body" }, 133 | { "ErrParList", "expected a variable name or '...' after ','" }, 134 | 135 | { "ErrLabel", "expected a label name after '::'" }, 136 | { "ErrCloseLabel", "expected '::' after the label" }, 137 | { "ErrGoto", "expected a label after 'goto'" }, 138 | { "ErrRetList", "expected an expression after ',' in the return statement" }, 139 | 140 | { "ErrVarList", "expected a variable name after ','" }, 141 | { "ErrExprList", "expected an expression after ','" }, 142 | 143 | { "ErrOrExpr", "expected an expression after 'or'" }, 144 | { "ErrAndExpr", "expected an expression after 'and'" }, 145 | { "ErrRelExpr", "expected an expression after the relational operator" }, 146 | { "ErrBOrExpr", "expected an expression after '|'" }, 147 | { "ErrBXorExpr", "expected an expression after '~'" }, 148 | { "ErrBAndExpr", "expected an expression after '&'" }, 149 | { "ErrShiftExpr", "expected an expression after the bit shift" }, 150 | { "ErrConcatExpr", "expected an expression after '..'" }, 151 | { "ErrAddExpr", "expected an expression after the additive operator" }, 152 | { "ErrMulExpr", "expected an expression after the multiplicative operator" }, 153 | { "ErrUnaryExpr", "expected an expression after the unary operator" }, 154 | { "ErrPowExpr", "expected an expression after '^'" }, 155 | 156 | { "ErrExprParen", "expected an expression after '('" }, 157 | { "ErrCParenExpr", "expected ')' to close the expression" }, 158 | { "ErrNameIndex", "expected a field name after '.'" }, 159 | { "ErrExprIndex", "expected an expression after '['" }, 160 | { "ErrCBracketIndex", "expected ']' to close the indexing expression" }, 161 | { "ErrNameMeth", "expected a method name after ':'" }, 162 | { "ErrMethArgs", "expected some arguments for the method call (or '()')" }, 163 | 164 | { "ErrArgList", "expected an expression after ',' in the argument list" }, 165 | { "ErrCParenArgs", "expected ')' to close the argument list" }, 166 | 167 | { "ErrCBraceTable", "expected '}' to close the table constructor" }, 168 | { "ErrEqField", "expected '=' after the table key" }, 169 | { "ErrExprField", "expected an expression after '='" }, 170 | { "ErrExprFKey", "expected an expression after '[' for the table key" }, 171 | { "ErrCBracketFKey", "expected ']' to close the table key" }, 172 | 173 | { "ErrDigitHex", "expected one or more hexadecimal digits after '0x'" }, 174 | { "ErrDigitDeci", "expected one or more digits after the decimal point" }, 175 | { "ErrDigitExpo", "expected one or more digits for the exponent" }, 176 | 177 | { "ErrQuote", "unclosed string" }, 178 | { "ErrHexEsc", "expected exactly two hexadecimal digits after '\\x'" }, 179 | { "ErrOBraceUEsc", "expected '{' after '\\u'" }, 180 | { "ErrDigitUEsc", "expected one or more hexadecimal digits for the UTF-8 code point" }, 181 | { "ErrCBraceUEsc", "expected '}' after the code point" }, 182 | { "ErrEscSeq", "invalid escape sequence" }, 183 | { "ErrCloseLStr", "unclosed long string" }, 184 | } 185 | 186 | local function throw(label) 187 | label = "Err" .. label 188 | for i, labelinfo in ipairs(labels) do 189 | if labelinfo[1] == label then 190 | return T(i) 191 | end 192 | end 193 | 194 | error("Label not found: " .. label) 195 | end 196 | 197 | local function expect (patt, label) 198 | return patt + throw(label) 199 | end 200 | 201 | 202 | -- regular combinators and auxiliary functions 203 | 204 | local function token (patt) 205 | return patt * V"Skip" 206 | end 207 | 208 | local function sym (str) 209 | return token(P(str)) 210 | end 211 | 212 | local function kw (str) 213 | return token(P(str) * -V"IdRest") 214 | end 215 | 216 | local function tagC (tag, patt) 217 | return Ct(Cg(Cp(), "pos") * Cg(Cc(tag), "tag") * patt * Cg(Cp(), "posEnd")) 218 | end 219 | 220 | local function unaryOp (op, e) 221 | return { tag = "Op", pos = e.pos, posEnd = e.posEnd, [1] = op, [2] = e } 222 | end 223 | 224 | local function binaryOp (e1, op, e2) 225 | if not op then 226 | return e1 227 | end 228 | 229 | local node = { tag = "Op", pos = e1.pos, posEnd = e1.posEnd, [1] = op, [2] = e1, [3] = e2 } 230 | 231 | if op == "ne" then 232 | node[1] = "eq" 233 | node = unaryOp("not", node) 234 | elseif op == "gt" then 235 | node[1], node[2], node[3] = "lt", e2, e1 236 | elseif op == "ge" then 237 | node[1], node[2], node[3] = "le", e2, e1 238 | end 239 | 240 | return node 241 | end 242 | 243 | local function sepBy (patt, sep, label) 244 | if label then 245 | return patt * Cg(sep * expect(patt, label))^0 246 | else 247 | return patt * Cg(sep * patt)^0 248 | end 249 | end 250 | 251 | local function chainOp (patt, sep, label) 252 | return Cf(sepBy(patt, sep, label), binaryOp) 253 | end 254 | 255 | local function commaSep (patt, label) 256 | return sepBy(patt, sym(","), label) 257 | end 258 | 259 | local function tagDo (block) 260 | block.tag = "Do" 261 | return block 262 | end 263 | 264 | local function fixFuncStat (func) 265 | if func[1].is_method then table.insert(func[2][1], 1, { tag = "Id", [1] = "self" }) end 266 | func[1] = {func[1]} 267 | func[2] = {func[2]} 268 | return func 269 | end 270 | 271 | local function addDots (params, dots) 272 | if dots then table.insert(params, dots) end 273 | return params 274 | end 275 | 276 | local function insertIndex (t, index) 277 | return { tag = "Index", pos = t.pos, posEnd = t.posEnd, [1] = t, [2] = index } 278 | end 279 | 280 | local function markMethod(t, method) 281 | if method then 282 | return { tag = "Index", pos = t.pos, posEnd = t.posEnd, is_method = true, [1] = t, [2] = method } 283 | end 284 | return t 285 | end 286 | 287 | local function makeIndexOrCall (t1, t2) 288 | if t2.tag == "Call" or t2.tag == "Invoke" then 289 | local t = { tag = t2.tag, pos = t1.pos, posEnd = t1.posEnd, [1] = t1 } 290 | for k, v in ipairs(t2) do 291 | table.insert(t, v) 292 | end 293 | return t 294 | end 295 | return { tag = "Index", pos = t1.pos, posEnd = t1.posEnd, [1] = t1, [2] = t2[1] } 296 | end 297 | 298 | -- grammar 299 | local G = {} 300 | G["5.3"] = { V"Lua", 301 | Lua = V"Shebang"^-1 * V"Skip" * V"Block" * expect(P(-1), "Extra"); 302 | Shebang = P"#!" * (P(1) - P"\n")^0; 303 | 304 | Block = tagC("Block", V"Stat"^0 * V"RetStat"^-1); 305 | Stat = V"IfStat" + V"DoStat" + V"WhileStat" + V"RepeatStat" + V"ForStat" 306 | + V"LocalStat" + V"FuncStat" + V"BreakStat" + V"LabelStat" + V"GoToStat" 307 | + V"FuncCall" + V"Assignment" + sym(";") + -V"BlockEnd" * throw("InvalidStat"); 308 | BlockEnd = P"return" + "end" + "elseif" + "else" + "until" + -1; 309 | 310 | IfStat = tagC("If", V"IfPart" * V"ElseIfPart"^0 * V"ElsePart"^-1 * expect(kw("end"), "EndIf")); 311 | IfPart = kw("if") * expect(V"Expr", "ExprIf") * expect(kw("then"), "ThenIf") * V"Block"; 312 | ElseIfPart = kw("elseif") * expect(V"Expr", "ExprEIf") * expect(kw("then"), "ThenEIf") * V"Block"; 313 | ElsePart = kw("else") * V"Block"; 314 | 315 | DoStat = kw("do") * V"Block" * expect(kw("end"), "EndDo") / tagDo; 316 | WhileStat = tagC("While", kw("while") * expect(V"Expr", "ExprWhile") * V"WhileBody"); 317 | WhileBody = expect(kw("do"), "DoWhile") * V"Block" * expect(kw("end"), "EndWhile"); 318 | RepeatStat = tagC("Repeat", kw("repeat") * V"Block" * expect(kw("until"), "UntilRep") * expect(V"Expr", "ExprRep")); 319 | 320 | ForStat = kw("for") * expect(V"ForNum" + V"ForIn", "ForRange") * expect(kw("end"), "EndFor"); 321 | ForNum = tagC("Fornum", V"Id" * sym("=") * V"NumRange" * V"ForBody"); 322 | NumRange = expect(V"Expr", "ExprFor1") * expect(sym(","), "CommaFor") *expect(V"Expr", "ExprFor2") 323 | * (sym(",") * expect(V"Expr", "ExprFor3"))^-1; 324 | ForIn = tagC("Forin", V"NameList" * expect(kw("in"), "InFor") * expect(V"ExprList", "EListFor") * V"ForBody"); 325 | ForBody = expect(kw("do"), "DoFor") * V"Block"; 326 | 327 | LocalStat = kw("local") * expect(V"LocalFunc" + V"LocalAssign", "DefLocal"); 328 | LocalFunc = tagC("Localrec", kw("function") * expect(V"Id", "NameLFunc") * V"FuncBody") / fixFuncStat; 329 | LocalAssign = tagC("Local", V"NameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc()))); 330 | Assignment = tagC("Set", V"VarList" * sym("=") * expect(V"ExprList", "EListAssign")); 331 | 332 | FuncStat = tagC("Set", kw("function") * expect(V"FuncName", "FuncName") * V"FuncBody") / fixFuncStat; 333 | FuncName = Cf(V"Id" * (sym(".") * expect(V"StrId", "NameFunc1"))^0, insertIndex) 334 | * (sym(":") * expect(V"StrId", "NameFunc2"))^-1 / markMethod; 335 | FuncBody = tagC("Function", V"FuncParams" * V"Block" * expect(kw("end"), "EndFunc")); 336 | FuncParams = expect(sym("("), "OParenPList") * V"ParList" * expect(sym(")"), "CParenPList"); 337 | ParList = V"NameList" * (sym(",") * expect(tagC("Dots", sym("...")), "ParList"))^-1 / addDots 338 | + Ct(tagC("Dots", sym("..."))) 339 | + Ct(Cc()); -- Cc({}) generates a bug since the {} would be shared across parses 340 | 341 | LabelStat = tagC("Label", sym("::") * expect(V"Name", "Label") * expect(sym("::"), "CloseLabel")); 342 | GoToStat = tagC("Goto", kw("goto") * expect(V"Name", "Goto")); 343 | BreakStat = tagC("Break", kw("break")); 344 | RetStat = tagC("Return", kw("return") * commaSep(V"Expr", "RetList")^-1 * sym(";")^-1); 345 | 346 | NameList = tagC("NameList", commaSep(V"Id")); 347 | VarList = tagC("VarList", commaSep(V"VarExpr", "VarList")); 348 | ExprList = tagC("ExpList", commaSep(V"Expr", "ExprList")); 349 | 350 | Expr = V"OrExpr"; 351 | OrExpr = chainOp(V"AndExpr", V"OrOp", "OrExpr"); 352 | AndExpr = chainOp(V"RelExpr", V"AndOp", "AndExpr"); 353 | RelExpr = chainOp(V"BOrExpr", V"RelOp", "RelExpr"); 354 | BOrExpr = chainOp(V"BXorExpr", V"BOrOp", "BOrExpr"); 355 | BXorExpr = chainOp(V"BAndExpr", V"BXorOp", "BXorExpr"); 356 | BAndExpr = chainOp(V"ShiftExpr", V"BAndOp", "BAndExpr"); 357 | ShiftExpr = chainOp(V"ConcatExpr", V"ShiftOp", "ShiftExpr"); 358 | ConcatExpr = V"AddExpr" * (V"ConcatOp" * expect(V"ConcatExpr", "ConcatExpr"))^-1 / binaryOp; 359 | AddExpr = chainOp(V"MulExpr", V"AddOp", "AddExpr"); 360 | MulExpr = chainOp(V"UnaryExpr", V"MulOp", "MulExpr"); 361 | UnaryExpr = V"UnaryOp" * expect(V"UnaryExpr", "UnaryExpr") / unaryOp 362 | + V"PowExpr"; 363 | PowExpr = V"SimpleExpr" * (V"PowOp" * expect(V"UnaryExpr", "PowExpr"))^-1 / binaryOp; 364 | 365 | SimpleExpr = tagC("Number", V"Number") 366 | + tagC("String", V"String") 367 | + tagC("Nil", kw("nil")) 368 | + tagC("False", kw("false")) 369 | + tagC("True", kw("true")) 370 | + tagC("Dots", sym("...")) 371 | + V"FuncDef" 372 | + V"Table" 373 | + V"SuffixedExpr"; 374 | 375 | FuncCall = Cmt(V"SuffixedExpr", function(s, i, exp) return exp.tag == "Call" or exp.tag == "Invoke", exp end); 376 | VarExpr = Cmt(V"SuffixedExpr", function(s, i, exp) return exp.tag == "Id" or exp.tag == "Index", exp end); 377 | 378 | SuffixedExpr = Cf(V"PrimaryExpr" * (V"Index" + V"Call")^0, makeIndexOrCall); 379 | PrimaryExpr = V"Id" + tagC("Paren", sym("(") * expect(V"Expr", "ExprParen") * expect(sym(")"), "CParenExpr")); 380 | Index = tagC("DotIndex", sym("." * -P".") * expect(V"StrId", "NameIndex")) 381 | + tagC("ArrayIndex", sym("[" * -P(S"=[")) * expect(V"Expr", "ExprIndex") * expect(sym("]"), "CBracketIndex")); 382 | Call = tagC("Invoke", Cg(sym(":" * -P":") * expect(V"StrId", "NameMeth") * expect(V"FuncArgs", "MethArgs"))) 383 | + tagC("Call", V"FuncArgs"); 384 | 385 | FuncDef = kw("function") * V"FuncBody"; 386 | FuncArgs = sym("(") * commaSep(V"Expr", "ArgList")^-1 * expect(sym(")"), "CParenArgs") 387 | + V"Table" 388 | + tagC("String", V"String"); 389 | 390 | Table = tagC("Table", sym("{") * V"FieldList"^-1 * expect(sym("}"), "CBraceTable")); 391 | FieldList = sepBy(V"Field", V"FieldSep") * V"FieldSep"^-1; 392 | Field = tagC("Pair", V"FieldKey" * expect(sym("="), "EqField") * expect(V"Expr", "ExprField")) 393 | + V"Expr"; 394 | FieldKey = sym("[" * -P(S"=[")) * expect(V"Expr", "ExprFKey") * expect(sym("]"), "CBracketFKey") 395 | + V"StrId" * #("=" * -P"="); 396 | FieldSep = sym(",") + sym(";"); 397 | 398 | Id = tagC("Id", V"Name"); 399 | StrId = tagC("String", V"Name"); 400 | 401 | -- lexer 402 | Skip = (V"Space" + V"Comment")^0; 403 | Space = space^1; 404 | Comment = P"--" * V"LongStr" / function () return end 405 | + P"--" * (P(1) - P"\n")^0; 406 | 407 | Name = token(-V"Reserved" * C(V"Ident")); 408 | Reserved = V"Keywords" * -V"IdRest"; 409 | Keywords = P"and" + "break" + "do" + "elseif" + "else" + "end" 410 | + "false" + "for" + "function" + "goto" + "if" + "in" 411 | + "local" + "nil" + "not" + "or" + "repeat" + "return" 412 | + "then" + "true" + "until" + "while"; 413 | Ident = V"IdStart" * V"IdRest"^0; 414 | IdStart = alpha + P"_"; 415 | IdRest = alnum + P"_"; 416 | 417 | Number = token((V"Hex" + V"Float" + V"Int") / tonumber); 418 | Hex = (P"0x" + "0X") * expect(xdigit^1, "DigitHex"); 419 | Float = V"Decimal" * V"Expo"^-1 420 | + V"Int" * V"Expo"; 421 | Decimal = digit^1 * "." * digit^0 422 | + P"." * -P"." * expect(digit^1, "DigitDeci"); 423 | Expo = S"eE" * S"+-"^-1 * expect(digit^1, "DigitExpo"); 424 | Int = digit^1; 425 | 426 | String = token(V"ShortStr" + V"LongStr"); 427 | ShortStr = P'"' * Cs((V"EscSeq" + (P(1)-S'"\n'))^0) * expect(P'"', "Quote") 428 | + P"'" * Cs((V"EscSeq" + (P(1)-S"'\n"))^0) * expect(P"'", "Quote"); 429 | 430 | EscSeq = P"\\" / "" -- remove backslash 431 | * ( P"a" / "\a" 432 | + P"b" / "\b" 433 | + P"f" / "\f" 434 | + P"n" / "\n" 435 | + P"r" / "\r" 436 | + P"t" / "\t" 437 | + P"v" / "\v" 438 | 439 | + P"\n" / "\n" 440 | + P"\r" / "\n" 441 | 442 | + P"\\" / "\\" 443 | + P"\"" / "\"" 444 | + P"\'" / "\'" 445 | 446 | + P"z" * space^0 / "" 447 | 448 | + digit * digit^-2 / tonumber / string.char 449 | + P"x" * expect(C(xdigit * xdigit), "HexEsc") * Cc(16) / tonumber / string.char 450 | + P"u" * expect("{", "OBraceUEsc") 451 | * expect(C(xdigit^1), "DigitUEsc") * Cc(16) 452 | * expect("}", "CBraceUEsc") 453 | / tonumber 454 | / (utf8 and utf8.char or string.char) -- true max is \u{10FFFF} 455 | -- utf8.char needs Lua 5.3 456 | -- string.char works only until \u{FF} 457 | 458 | + throw("EscSeq") 459 | ); 460 | 461 | LongStr = V"Open" * C((P(1) - V"CloseEq")^0) * expect(V"Close", "CloseLStr") / function (s, eqs) return s end; 462 | Open = "[" * Cg(V"Equals", "openEq") * "[" * P"\n"^-1; 463 | Close = "]" * C(V"Equals") * "]"; 464 | Equals = P"="^0; 465 | CloseEq = Cmt(V"Close" * Cb("openEq"), function (s, i, closeEq, openEq) return #openEq == #closeEq end); 466 | 467 | OrOp = kw("or") / "or"; 468 | AndOp = kw("and") / "and"; 469 | RelOp = sym("~=") / "ne" 470 | + sym("==") / "eq" 471 | + sym("<=") / "le" 472 | + sym(">=") / "ge" 473 | + sym("<") / "lt" 474 | + sym(">") / "gt"; 475 | BOrOp = sym("|") / "bor"; 476 | BXorOp = sym("~" * -P"=") / "bxor"; 477 | BAndOp = sym("&") / "band"; 478 | ShiftOp = sym("<<") / "shl" 479 | + sym(">>") / "shr"; 480 | ConcatOp = sym("..") / "concat"; 481 | AddOp = sym("+") / "add" 482 | + sym("-") / "sub"; 483 | MulOp = sym("*") / "mul" 484 | + sym("//") / "idiv" 485 | + sym("/") / "div" 486 | + sym("%") / "mod"; 487 | UnaryOp = kw("not") / "not" 488 | + sym("-") / "unm" 489 | + sym("#") / "len" 490 | + sym("~") / "bnot"; 491 | PowOp = sym("^") / "pow"; 492 | } 493 | local function copy(t) 494 | local nt = {} 495 | for k, v in pairs(t) do 496 | nt[k] = v 497 | end 498 | return nt 499 | end 500 | 501 | G["5.2"] = copy(G["5.3"]) 502 | -- remove bitops 503 | G["5.2"].RelExpr = chainOp(V"ConcatExpr", V"RelOp", "RelExpr") 504 | 505 | G["5.2"].BOrOp = nil 506 | G["5.2"].BOrExpr = nil 507 | G["5.2"].BXorOp = nil 508 | G["5.2"].BXorExpr = nil 509 | G["5.2"].BAndOp = nil 510 | G["5.2"].BAndExpr = nil 511 | G["5.2"].ShiftOp = nil 512 | G["5.2"].ShiftExpr = nil 513 | 514 | -- remove integer division "//" 515 | G["5.2"].MulOp = 516 | sym("*") / "mul" 517 | + sym("/") / "div" 518 | + sym("%") / "mod"; 519 | -- remove unicode codepoints 520 | G["5.2"].EscSeq = P"\\" / "" * ( 521 | P"a" / "\a" 522 | + P"b" / "\b" 523 | + P"f" / "\f" 524 | + P"n" / "\n" 525 | + P"r" / "\r" 526 | + P"t" / "\t" 527 | + P"v" / "\v" 528 | 529 | + P"\n" / "\n" 530 | + P"\r" / "\n" 531 | 532 | + P"\\" / "\\" 533 | + P"\"" / "\"" 534 | + P"\'" / "\'" 535 | 536 | + P"z" * space^0 / "" 537 | 538 | + digit * digit^-2 / tonumber / string.char 539 | + P"x" * expect(C(xdigit * xdigit), "HexEsc") * Cc(16) / tonumber / string.char 540 | 541 | + throw("EscSeq") 542 | ); 543 | 544 | G["5.1"] = copy(G["5.2"]) 545 | -- remove goto/label 546 | G["5.1"].LabelStat = nil 547 | G["5.1"].GoToStat = nil 548 | G["5.1"].Stat = V"IfStat" + V"DoStat" + V"WhileStat" + V"RepeatStat" + V"ForStat" 549 | + V"LocalStat" + V"FuncStat" + V"BreakStat" + V"FuncCall" 550 | + V"Assignment" + sym(";") + -V"BlockEnd" * throw("InvalidStat"); 551 | -- remove hex. as far as I can tell star doesn't actually exist 552 | G["5.1"].EscSeq = P"\\" / "" * ( 553 | P"a" / "\a" 554 | + P"b" / "\b" 555 | + P"f" / "\f" 556 | + P"n" / "\n" 557 | + P"r" / "\r" 558 | + P"t" / "\t" 559 | + P"v" / "\v" 560 | 561 | + P"\n" / "\n" 562 | + P"\r" / "\n" 563 | 564 | + P"\\" / "\\" 565 | + P"\"" / "\"" 566 | + P"\'" / "\'" 567 | 568 | + P"z" * space^0 / "" 569 | 570 | + digit * digit^-2 / tonumber / string.char 571 | 572 | + throw("EscSeq") 573 | ); 574 | 575 | G["luajit"] = copy(G["5.2"]) 576 | 577 | -- Luajit supports complex numbers as 69i, and cdata numbers as 69LL or 69ULL 578 | -- The parser for Lua source code treats numeric literals with the suffixes LL 579 | -- or ULL as signed or unsigned 64 bit integers. Case doesn't matter, but 580 | -- uppercase is recommended for readability. It handles both decimal (42LL) and 581 | -- hexadecimal (0x2aLL) literals. The imaginary part of complex numbers can be 582 | -- specified by suffixing number literals with i or I, e.g. 12.5i. Caveat: 583 | -- you'll need to use 1i to get an imaginary part with the value one, since i 584 | -- itself still refers to a variable named i. 585 | G["luajit"].Hex = (P"0x" + "0X") * expect(xdigit^1, "DigitHex") * V"Long"^-1; 586 | G["luajit"].Float = V"Decimal" * V"Expo"^-1 587 | + V"Int" * V"Expo"; 588 | G["luajit"].Decimal = digit^1 * "." * digit^0 589 | + P"." * -P"." * expect(digit^1, "DigitDeci"); 590 | G["luajit"].Expo = S"eE" * S"+-"^-1 * expect(digit^1, "DigitExpo"); 591 | G["luajit"].Int = digit^1 * V"Long"^-1; 592 | G["luajit"].Long = (S"Uu"^-1) * S"Ll" * S"Ll"; 593 | 594 | local parser = {} 595 | 596 | local validator = require("tarantool-lsp.lua-parser.validator") 597 | local validate = validator.validate 598 | local syntaxerror = validator.syntaxerror 599 | 600 | function parser.parse(subject, filename, version) 601 | if not G[version] then 602 | error(tostring(version) .. " is not a supported lua version", 2) 603 | end 604 | local errorinfo = { subject = subject, filename = filename } 605 | lpeg.setmaxstack(1000) 606 | local ast, label, errpos = lpeg.match(G["5.3"], subject, nil, errorinfo) 607 | if not ast then 608 | local errmsg = labels[label][2] 609 | return ast, syntaxerror(errorinfo, errpos, errmsg) 610 | end 611 | 612 | return validate(ast, errorinfo) 613 | end 614 | 615 | function parser.pp(expr) 616 | if type(expr) ~= "table" then return expr end 617 | if expr.tag == "Number" then return tostring(expr[1]) end 618 | if expr.tag == "String" then return ("%q"):format(expr[1]) end 619 | if expr.tag == "Id" then return ("%s"):format(expr[1]) end 620 | 621 | local spit = {} 622 | for _, e in ipairs(expr) do 623 | table.insert(spit, parser.pp(e)) 624 | end 625 | spit = table.concat(spit, ", ") 626 | 627 | return string.format("`%s { %s }", expr.tag, spit) 628 | end 629 | 630 | return parser 631 | -------------------------------------------------------------------------------- /tarantool-lsp/lua-parser/scope.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This module implements functions that handle scoping rules 3 | ]] 4 | local scope = {} 5 | 6 | function scope.lineno (s, i) 7 | if i == 1 then return 1, 1 end 8 | local l, lastline = 0, "" 9 | s = s:sub(1, i) .. "\n" 10 | for line in s:gmatch("[^\n]*[\n]") do 11 | l = l + 1 12 | lastline = line 13 | end 14 | local c = lastline:len() - 1 15 | return l, c ~= 0 and c or 1 16 | end 17 | 18 | function scope.new_scope (env) 19 | if not env.scope then 20 | env.scope = 0 21 | else 22 | env.scope = env.scope + 1 23 | end 24 | local scope = env.scope 25 | env.maxscope = scope 26 | env[scope] = {} 27 | env[scope]["label"] = {} 28 | env[scope]["local"] = {} 29 | env[scope]["goto"] = {} 30 | end 31 | 32 | function scope.begin_scope (env) 33 | env.scope = env.scope + 1 34 | end 35 | 36 | function scope.end_scope (env) 37 | env.scope = env.scope - 1 38 | end 39 | 40 | function scope.new_function (env) 41 | if not env.fscope then 42 | env.fscope = 0 43 | else 44 | env.fscope = env.fscope + 1 45 | end 46 | local fscope = env.fscope 47 | env["function"][fscope] = {} 48 | end 49 | 50 | function scope.begin_function (env) 51 | env.fscope = env.fscope + 1 52 | end 53 | 54 | function scope.end_function (env) 55 | env.fscope = env.fscope - 1 56 | end 57 | 58 | function scope.begin_loop (env) 59 | if not env.loop then 60 | env.loop = 1 61 | else 62 | env.loop = env.loop + 1 63 | end 64 | end 65 | 66 | function scope.end_loop (env) 67 | env.loop = env.loop - 1 68 | end 69 | 70 | function scope.insideloop (env) 71 | return env.loop and env.loop > 0 72 | end 73 | 74 | return scope 75 | -------------------------------------------------------------------------------- /tarantool-lsp/lua-parser/validator.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This module impements a validator for the AST 3 | ]] 4 | local scope = require "tarantool-lsp.lua-parser.scope" 5 | 6 | local lineno = scope.lineno 7 | local new_scope, end_scope = scope.new_scope, scope.end_scope 8 | local new_function, end_function = scope.new_function, scope.end_function 9 | local begin_loop, end_loop = scope.begin_loop, scope.end_loop 10 | local insideloop = scope.insideloop 11 | 12 | -- creates an error message for the input string 13 | local function syntaxerror (errorinfo, pos, msg) 14 | local l, c = lineno(errorinfo.subject, pos) 15 | --local error_msg = "%s:%d:%d: syntax error, %s" 16 | --return string.format(error_msg, errorinfo.filename, l, c, msg) 17 | return { 18 | file = errorinfo.filename, 19 | line = l, 20 | column = c, 21 | message = msg 22 | } 23 | end 24 | 25 | local function exist_label (env, scope, stm) 26 | local l = stm[1] 27 | for s=scope, 0, -1 do 28 | if env[s]["label"][l] then return true end 29 | end 30 | return false 31 | end 32 | 33 | local function set_label (env, label, pos) 34 | local scope = env.scope 35 | local l = env[scope]["label"][label] 36 | if not l then 37 | env[scope]["label"][label] = { name = label, pos = pos } 38 | return true 39 | else 40 | local msg = "label '%s' already defined at line %d" 41 | local line = lineno(env.errorinfo.subject, l.pos) 42 | msg = string.format(msg, label, line) 43 | return nil, syntaxerror(env.errorinfo, pos, msg) 44 | end 45 | end 46 | 47 | local function set_pending_goto (env, stm) 48 | local scope = env.scope 49 | table.insert(env[scope]["goto"], stm) 50 | return true 51 | end 52 | 53 | local function verify_pending_gotos (env) 54 | for s=env.maxscope, 0, -1 do 55 | for k, v in ipairs(env[s]["goto"]) do 56 | if not exist_label(env, s, v) then 57 | local msg = "no visible label '%s' for " 58 | msg = string.format(msg, v[1]) 59 | return nil, syntaxerror(env.errorinfo, v.pos, msg) 60 | end 61 | end 62 | end 63 | return true 64 | end 65 | 66 | local function set_vararg (env, is_vararg) 67 | env["function"][env.fscope].is_vararg = is_vararg 68 | end 69 | 70 | local traverse_stm, traverse_exp, traverse_var 71 | local traverse_block, traverse_explist, traverse_varlist, traverse_parlist 72 | 73 | function traverse_parlist (env, parlist) 74 | local len = #parlist 75 | local is_vararg = false 76 | if len > 0 and parlist[len].tag == "Dots" then 77 | is_vararg = true 78 | end 79 | set_vararg(env, is_vararg) 80 | return true 81 | end 82 | 83 | local function traverse_function (env, exp) 84 | new_function(env) 85 | new_scope(env) 86 | local status, msg = traverse_parlist(env, exp[1]) 87 | if not status then return status, msg end 88 | status, msg = traverse_block(env, exp[2]) 89 | if not status then return status, msg end 90 | end_scope(env) 91 | end_function(env) 92 | return true 93 | end 94 | 95 | local function traverse_op (env, exp) 96 | local status, msg = traverse_exp(env, exp[2]) 97 | if not status then return status, msg end 98 | if exp[3] then 99 | status, msg = traverse_exp(env, exp[3]) 100 | if not status then return status, msg end 101 | end 102 | return true 103 | end 104 | 105 | local function traverse_paren (env, exp) 106 | local status, msg = traverse_exp(env, exp[1]) 107 | if not status then return status, msg end 108 | return true 109 | end 110 | 111 | local function traverse_table (env, fieldlist) 112 | for k, v in ipairs(fieldlist) do 113 | local tag = v.tag 114 | if tag == "Pair" then 115 | local status, msg = traverse_exp(env, v[1]) 116 | if not status then return status, msg end 117 | status, msg = traverse_exp(env, v[2]) 118 | if not status then return status, msg end 119 | else 120 | local status, msg = traverse_exp(env, v) 121 | if not status then return status, msg end 122 | end 123 | end 124 | return true 125 | end 126 | 127 | local function traverse_vararg (env, exp) 128 | if not env["function"][env.fscope].is_vararg then 129 | local msg = "cannot use '...' outside a vararg function" 130 | return nil, syntaxerror(env.errorinfo, exp.pos, msg) 131 | end 132 | return true 133 | end 134 | 135 | local function traverse_call (env, call) 136 | local status, msg = traverse_exp(env, call[1]) 137 | if not status then return status, msg end 138 | for i=2, #call do 139 | status, msg = traverse_exp(env, call[i]) 140 | if not status then return status, msg end 141 | end 142 | return true 143 | end 144 | 145 | local function traverse_invoke (env, invoke) 146 | local status, msg = traverse_exp(env, invoke[1]) 147 | if not status then return status, msg end 148 | for i=3, #invoke do 149 | status, msg = traverse_exp(env, invoke[i]) 150 | if not status then return status, msg end 151 | end 152 | return true 153 | end 154 | 155 | local function traverse_assignment (env, stm) 156 | local status, msg = traverse_varlist(env, stm[1]) 157 | if not status then return status, msg end 158 | status, msg = traverse_explist(env, stm[2]) 159 | if not status then return status, msg end 160 | return true 161 | end 162 | 163 | local function traverse_break (env, stm) 164 | if not insideloop(env) then 165 | local msg = " not inside a loop" 166 | return nil, syntaxerror(env.errorinfo, stm.pos, msg) 167 | end 168 | return true 169 | end 170 | 171 | local function traverse_forin (env, stm) 172 | begin_loop(env) 173 | new_scope(env) 174 | local status, msg = traverse_explist(env, stm[2]) 175 | if not status then return status, msg end 176 | status, msg = traverse_block(env, stm[3]) 177 | if not status then return status, msg end 178 | end_scope(env) 179 | end_loop(env) 180 | return true 181 | end 182 | 183 | local function traverse_fornum (env, stm) 184 | local status, msg 185 | begin_loop(env) 186 | new_scope(env) 187 | status, msg = traverse_exp(env, stm[2]) 188 | if not status then return status, msg end 189 | status, msg = traverse_exp(env, stm[3]) 190 | if not status then return status, msg end 191 | if stm[5] then 192 | status, msg = traverse_exp(env, stm[4]) 193 | if not status then return status, msg end 194 | status, msg = traverse_block(env, stm[5]) 195 | if not status then return status, msg end 196 | else 197 | status, msg = traverse_block(env, stm[4]) 198 | if not status then return status, msg end 199 | end 200 | end_scope(env) 201 | end_loop(env) 202 | return true 203 | end 204 | 205 | local function traverse_goto (env, stm) 206 | local status, msg = set_pending_goto(env, stm) 207 | if not status then return status, msg end 208 | return true 209 | end 210 | 211 | local function traverse_if (env, stm) 212 | local len = #stm 213 | if len % 2 == 0 then 214 | for i=1, len, 2 do 215 | local status, msg = traverse_exp(env, stm[i]) 216 | if not status then return status, msg end 217 | status, msg = traverse_block(env, stm[i+1]) 218 | if not status then return status, msg end 219 | end 220 | else 221 | for i=1, len-1, 2 do 222 | local status, msg = traverse_exp(env, stm[i]) 223 | if not status then return status, msg end 224 | status, msg = traverse_block(env, stm[i+1]) 225 | if not status then return status, msg end 226 | end 227 | local status, msg = traverse_block(env, stm[len]) 228 | if not status then return status, msg end 229 | end 230 | return true 231 | end 232 | 233 | local function traverse_label (env, stm) 234 | local status, msg = set_label(env, stm[1], stm.pos) 235 | if not status then return status, msg end 236 | return true 237 | end 238 | 239 | local function traverse_let (env, stm) 240 | local status, msg = traverse_explist(env, stm[2]) 241 | if not status then return status, msg end 242 | return true 243 | end 244 | 245 | local function traverse_letrec (env, stm) 246 | local status, msg = traverse_exp(env, stm[2][1]) 247 | if not status then return status, msg end 248 | return true 249 | end 250 | 251 | local function traverse_repeat (env, stm) 252 | begin_loop(env) 253 | local status, msg = traverse_block(env, stm[1]) 254 | if not status then return status, msg end 255 | status, msg = traverse_exp(env, stm[2]) 256 | if not status then return status, msg end 257 | end_loop(env) 258 | return true 259 | end 260 | 261 | local function traverse_return (env, stm) 262 | local status, msg = traverse_explist(env, stm) 263 | if not status then return status, msg end 264 | return true 265 | end 266 | 267 | local function traverse_while (env, stm) 268 | begin_loop(env) 269 | local status, msg = traverse_exp(env, stm[1]) 270 | if not status then return status, msg end 271 | status, msg = traverse_block(env, stm[2]) 272 | if not status then return status, msg end 273 | end_loop(env) 274 | return true 275 | end 276 | 277 | function traverse_var (env, var) 278 | local tag = var.tag 279 | if tag == "Id" then -- `Id{ } 280 | return true 281 | elseif tag == "Index" then -- `Index{ expr expr } 282 | local status, msg = traverse_exp(env, var[1]) 283 | if not status then return status, msg end 284 | status, msg = traverse_exp(env, var[2]) 285 | if not status then return status, msg end 286 | return true 287 | else 288 | error("expecting a variable, but got a " .. tag) 289 | end 290 | end 291 | 292 | function traverse_varlist (env, varlist) 293 | for k, v in ipairs(varlist) do 294 | local status, msg = traverse_var(env, v) 295 | if not status then return status, msg end 296 | end 297 | return true 298 | end 299 | 300 | function traverse_exp (env, exp) 301 | local tag = exp.tag 302 | if tag == "Nil" or 303 | tag == "True" or 304 | tag == "False" or 305 | tag == "Number" or -- `Number{ } 306 | tag == "String" then -- `String{ } 307 | return true 308 | elseif tag == "Dots" then 309 | return traverse_vararg(env, exp) 310 | elseif tag == "Function" then -- `Function{ { `Id{ }* `Dots? } block } 311 | return traverse_function(env, exp) 312 | elseif tag == "Table" then -- `Table{ ( `Pair{ expr expr } | expr )* } 313 | return traverse_table(env, exp) 314 | elseif tag == "Op" then -- `Op{ opid expr expr? } 315 | return traverse_op(env, exp) 316 | elseif tag == "Paren" then -- `Paren{ expr } 317 | return traverse_paren(env, exp) 318 | elseif tag == "Call" then -- `Call{ expr expr* } 319 | return traverse_call(env, exp) 320 | elseif tag == "Invoke" then -- `Invoke{ expr `String{ expr* } 321 | return traverse_invoke(env, exp) 322 | elseif tag == "Id" or -- `Id{ } 323 | tag == "Index" then -- `Index{ expr expr } 324 | return traverse_var(env, exp) 325 | else 326 | error("expecting an expression, but got a " .. tag) 327 | end 328 | end 329 | 330 | function traverse_explist (env, explist) 331 | for k, v in ipairs(explist) do 332 | local status, msg = traverse_exp(env, v) 333 | if not status then return status, msg end 334 | end 335 | return true 336 | end 337 | 338 | function traverse_stm (env, stm) 339 | local tag = stm.tag 340 | if tag == "Do" then -- `Do{ stat* } 341 | return traverse_block(env, stm) 342 | elseif tag == "Set" then -- `Set{ {lhs+} {expr+} } 343 | return traverse_assignment(env, stm) 344 | elseif tag == "While" then -- `While{ expr block } 345 | return traverse_while(env, stm) 346 | elseif tag == "Repeat" then -- `Repeat{ block expr } 347 | return traverse_repeat(env, stm) 348 | elseif tag == "If" then -- `If{ (expr block)+ block? } 349 | return traverse_if(env, stm) 350 | elseif tag == "Fornum" then -- `Fornum{ ident expr expr expr? block } 351 | return traverse_fornum(env, stm) 352 | elseif tag == "Forin" then -- `Forin{ {ident+} {expr+} block } 353 | return traverse_forin(env, stm) 354 | elseif tag == "Local" then -- `Local{ {ident+} {expr+}? } 355 | return traverse_let(env, stm) 356 | elseif tag == "Localrec" then -- `Localrec{ ident expr } 357 | return traverse_letrec(env, stm) 358 | elseif tag == "Goto" then -- `Goto{ } 359 | return traverse_goto(env, stm) 360 | elseif tag == "Label" then -- `Label{ } 361 | return traverse_label(env, stm) 362 | elseif tag == "Return" then -- `Return{ * } 363 | return traverse_return(env, stm) 364 | elseif tag == "Break" then 365 | return traverse_break(env, stm) 366 | elseif tag == "Call" then -- `Call{ expr expr* } 367 | return traverse_call(env, stm) 368 | elseif tag == "Invoke" then -- `Invoke{ expr `String{ } expr* } 369 | return traverse_invoke(env, stm) 370 | elseif tag == "Comment" then 371 | return 372 | else 373 | error("expecting a statement, but got a " .. tag) 374 | end 375 | end 376 | 377 | function traverse_block (env, block) 378 | local l = {} 379 | new_scope(env) 380 | for k, v in ipairs(block) do 381 | local status, msg = traverse_stm(env, v) 382 | if not status then return status, msg end 383 | end 384 | end_scope(env) 385 | return true 386 | end 387 | 388 | 389 | local function traverse (ast, errorinfo) 390 | assert(type(ast) == "table") 391 | assert(type(errorinfo) == "table") 392 | local env = { errorinfo = errorinfo, ["function"] = {} } 393 | new_function(env) 394 | set_vararg(env, true) 395 | local status, msg = traverse_block(env, ast) 396 | if not status then return status, msg end 397 | end_function(env) 398 | status, msg = verify_pending_gotos(env) 399 | if not status then return status, msg end 400 | return ast 401 | end 402 | 403 | return { validate = traverse, syntaxerror = syntaxerror } 404 | -------------------------------------------------------------------------------- /tarantool-lsp/methods.lua: -------------------------------------------------------------------------------- 1 | -- the actual meat and potatos of the app 2 | local analyze = require 'tarantool-lsp.analyze' 3 | local rpc = require 'tarantool-lsp.rpc' 4 | local log = require 'tarantool-lsp.log' 5 | local utf = require 'tarantool-lsp.unicode' 6 | local docs = require('tarantool-lsp.tnt-doc.doc-manager') 7 | local json = require 'json' 8 | local unpack = table.unpack or unpack 9 | 10 | local fio = require('fio') 11 | local fun = require('fun') 12 | local console = require('console') 13 | local method_handlers = {} 14 | 15 | function method_handlers.initialize(config, params, id) 16 | if config.initialized then 17 | error("already initialized!") 18 | end 19 | if params.rootPath and params.rootPath ~= box.NULL then 20 | config.root = params.rootPath 21 | else 22 | if params.rootUri and params.rootUri ~= box.NULL then 23 | config.root = params.rootUri 24 | end 25 | end 26 | -- Some LSP clients doesn't provide capabilities for change 'trace' option 27 | -- and user options can override 28 | log.info("Config.root = %q", config.root) 29 | if not config.web_server and config.root and config.root ~= box.NULL then 30 | analyze.load_completerc(config, config.root) 31 | analyze.load_luacheckrc(config, config.root) 32 | end 33 | 34 | if not config.web_server then 35 | local libraries, err = docs:init({ completions_dir = config.completion_root }) 36 | if err ~= nil then 37 | log.info("Docs subsystem error: %s", err) 38 | end 39 | config.libraries = libraries 40 | end 41 | 42 | --ClientCapabilities = params.capabilities 43 | config.initialized = true 44 | -- hopefully this is modest enough 45 | return rpc.respond(id, { 46 | capabilities = { 47 | completionProvider = { 48 | triggerCharacters = {".",":"}, 49 | resolveProvider = false 50 | }, 51 | definitionProvider = true, 52 | textDocumentSync = { 53 | openClose = true, 54 | change = 1, -- 1 is non-incremental, 2 is incremental 55 | save = { includeText = true }, 56 | }, 57 | hoverProvider = true, 58 | documentSymbolProvider = true, 59 | -- referencesProvider = true , 60 | --documentHighlightProvider = false, 61 | --workspaceSymbolProvider = false, 62 | --codeActionProvider = false, 63 | --documentFormattingProvider = false, 64 | --documentRangeFormattingProvider = false, 65 | --renameProvider = false, 66 | } 67 | }) 68 | end 69 | 70 | method_handlers["textDocument/didOpen"] = function(config, params) 71 | config.documents[params.textDocument.uri] = params.textDocument 72 | analyze.refresh(config, params.textDocument) 73 | end 74 | 75 | method_handlers["textDocument/didChange"] = function(config, params) 76 | local document = analyze.document(config, params.textDocument) 77 | 78 | for _, patch in ipairs(params.contentChanges) do 79 | if (patch.range == nil and patch.rangeLength == nil) then 80 | document.text = patch.text 81 | else 82 | error("Incremental changes NYI") 83 | -- remove text from patch.range.start -> patch.range["end"] 84 | -- assert(patch.rangeLength == actual_length) 85 | -- then insert patch.text at origin point 86 | end 87 | end 88 | 89 | analyze.refresh(config, document) 90 | end 91 | 92 | method_handlers["textDocument/didSave"] = function(config, params) 93 | local uri = params.textDocument.uri 94 | local document = analyze.document(config, uri) 95 | -- FIXME: merge in details from params.textDocument 96 | analyze.refresh(config, document) 97 | end 98 | 99 | method_handlers["textDocument/didClose"] = function(config, params) 100 | config.documents[params.textDocument.uri] = nil 101 | end 102 | 103 | local function pick_scope(dirty, scopes, pos) 104 | assert(scopes ~= nil) 105 | local closest = nil 106 | local size = math.huge 107 | assert(#scopes > 0) 108 | if dirty then 109 | -- ignore posEnd, it's probably wrong 110 | for _, scope in ipairs(scopes) do 111 | local meta = getmetatable(scope) 112 | local dist = pos - meta.pos 113 | if meta.pos <= pos and dist < size then 114 | size = dist 115 | closest = scope 116 | end 117 | end 118 | else 119 | for _, scope in ipairs(scopes) do 120 | local meta = getmetatable(scope) 121 | local dist = meta.posEnd - meta.pos 122 | if meta.pos <= pos and meta.posEnd >= pos and dist < size then 123 | size = dist 124 | closest = scope 125 | end 126 | end 127 | end 128 | assert(closest, require('tarantool-lsp.inspect')(scopes)) 129 | 130 | return closest 131 | end 132 | 133 | local completionKinds = { 134 | Text = 1, 135 | Method = 2, 136 | Function = 3, 137 | Constructor = 4, 138 | Field = 5, 139 | Variable = 6, 140 | Class = 7, 141 | Interface = 8, 142 | Module = 9, 143 | Property = 10, 144 | Unit = 11, 145 | Value = 12, 146 | Enum = 13, 147 | Keyword = 14, 148 | Snippet = 15, 149 | Color = 16, 150 | File = 17, 151 | Reference = 18, 152 | } 153 | 154 | local function merge_(a, b) 155 | for k, v in pairs(b) do a[k] = v end 156 | end 157 | 158 | local function deduplicate_(tbl) 159 | local used = {} 160 | for i=#tbl, 1, -1 do 161 | if used[tbl[i]] then 162 | table.remove(tbl, i) 163 | else 164 | used[tbl[i]] = true 165 | end 166 | end 167 | end 168 | 169 | -- this is starting to get silly. 170 | local function make_completion_items(k, val, isField, isInvoke, isVariant) 171 | local item = { label = k } 172 | 173 | if val then 174 | if not isVariant and val.variants then 175 | local items = {} 176 | for _, variant in ipairs(val.variants) do 177 | local fakeval = {} 178 | merge_(fakeval, val) 179 | fakeval.variants = nil 180 | merge_(fakeval, variant) 181 | local i = make_completion_items(k, fakeval, isField, isInvoke, true) 182 | table.insert(items, i[1]) 183 | end 184 | return items 185 | end 186 | item.kind = isField and completionKinds.Field or completionKinds.Variable 187 | if val.tag == "Require" then 188 | -- this is a module 189 | item.kind = completionKinds.Module 190 | item.detail = ("M<%s>"):format(val.module) 191 | elseif val.tag == "Function" then 192 | -- generate function signature 193 | local val_is_method = false 194 | local sig = {} 195 | if val.arguments then 196 | for _, name in ipairs(val.arguments) do 197 | -- any function with at least one argument _could_ be a 198 | -- method 199 | if name.tag == "Dots" then 200 | table.insert(sig, "...") 201 | elseif isInvoke and not val_is_method then 202 | -- eat the first argument if it's not vararg 203 | val_is_method = true 204 | else 205 | local realname = name.displayName or name[1] or name.name 206 | assert(realname) 207 | table.insert(sig, realname) 208 | end 209 | 210 | end 211 | else 212 | table.insert(sig, " ??? ") 213 | end 214 | 215 | -- we still do the work associated with getting a signature, 216 | -- because that work informs whether or not an function is a 217 | -- method 218 | -- don't use isInvoke signatures because the builtins include the 219 | -- self param and we don't want to pass that in 220 | if val.signature and not isInvoke then 221 | sig = val.signature 222 | else 223 | sig = table.concat(sig, ", ") 224 | end 225 | 226 | -- if isInvoke and not val_is_method then 227 | -- -- don't list functions that aren't usable as methods 228 | -- return {} 229 | -- end 230 | 231 | local ret = "" 232 | if val.scope then 233 | local scope_mt = getmetatable(val.scope) 234 | if scope_mt._return then 235 | local sites = {} 236 | for _, site in ipairs(scope_mt._return) do 237 | local types, values, missingValues = {}, {}, false 238 | for _, r in ipairs(site) do 239 | if r.tag == "Literal" then 240 | local typename = ("<%s>"):format(r.tag:lower()) 241 | table.insert(types, typename) 242 | table.insert(values, string.lower(r.value)) 243 | elseif r.tag == "String" then 244 | table.insert(types, "") 245 | table.insert(values, string.format("%q", r.value)) 246 | elseif r.tag == "Id" then 247 | table.insert(types, r[1]) 248 | missingValues = true 249 | else 250 | -- not useful types 251 | local typename = ("<%s>"):format(r.tag:lower()) 252 | table.insert(types, typename) 253 | missingValues = true 254 | end 255 | end 256 | 257 | if missingValues then 258 | table.insert(sites, table.concat(types, ", ")) 259 | else 260 | table.insert(sites, table.concat(values, ", ")) 261 | end 262 | end 263 | deduplicate_(sites) 264 | ret = table.concat(sites, " | ") 265 | end 266 | elseif val.returns then 267 | ret = {} 268 | for _, r in ipairs(val.returns) do table.insert(ret, r.name) end 269 | ret = table.concat(ret, ", ") 270 | end 271 | 272 | if ret ~= "" then 273 | ret = string.format("-> %s", ret) 274 | end 275 | 276 | item.insertText = ("%s()"):format(k) 277 | item.label = ("%s(%s) %s"):format(k, sig, ret) 278 | item.documentation = val.description 279 | if isInvoke then 280 | item.kind = completionKinds.Method 281 | item.detail = "" 282 | else 283 | item.kind = completionKinds.Function 284 | item.detail = "" 285 | end 286 | 287 | item.detail = val.detail 288 | elseif val.tag == "Table" then 289 | item.detail = "
" 290 | item.documentation = val.description 291 | elseif val.tag == "String" then 292 | item.documentation = val.description 293 | if val.value then 294 | item.detail = string.format("%q", val.value) 295 | else 296 | item.detail = string.format("<%s>", val.tag) 297 | end 298 | elseif val.tag == "Literal" then 299 | item.documentation = val.description 300 | if val.value then 301 | item.detail = tostring(val.value) 302 | end 303 | elseif val.tag == "Arg" then 304 | item.detail = "" 305 | end 306 | end 307 | 308 | return {item} 309 | end 310 | 311 | local function iter_scope(scope) 312 | return coroutine.wrap(function() 313 | while scope do 314 | for key, nodes in pairs(scope) do 315 | local symbol, value = unpack(nodes) 316 | coroutine.yield(key, symbol, value) 317 | end 318 | local mt = getmetatable(scope) 319 | scope = mt and mt.__index 320 | end 321 | end) 322 | end 323 | 324 | 325 | local function make_position(document, pos) 326 | -- HACK: I'm not entirely sure why the AST is spitting out indexes larger 327 | -- than the string but clamping it seems fine 328 | if pos == document.text:len() + 1 then 329 | pos = pos - 1 330 | end 331 | assert(pos > 0) 332 | assert(pos <= document.text:len()) 333 | for i, line in ipairs(document.lines) do 334 | if line.start <= pos and line.start + line.text:len() >= pos then 335 | local text = line.text 336 | return { 337 | line = i-1, 338 | character = utf.to_codeunits(text, pos - line.start + 1) 339 | } 340 | end 341 | end 342 | error("invalid pos") 343 | end 344 | 345 | -- turn two index arguments into a range 346 | local function make_range(document, startidx, endidx) 347 | return { 348 | start = make_position(document, startidx), 349 | ["end"] = make_position(document, endidx) 350 | } 351 | end 352 | 353 | local function make_location(document, symbol) 354 | assert(symbol.pos) 355 | assert(symbol.posEnd) 356 | return { 357 | uri = document.uri, 358 | range = make_range(document, symbol.pos, symbol.posEnd) 359 | } 360 | end 361 | 362 | --- Returns line object, line index, and global index for an LSP position. 363 | -- line index and global index are measured in bytes, starting from 1 364 | local function line_for(document, pos) 365 | local line = document.lines[pos.line+1] 366 | local char = utf.to_bytes(line.text, pos.character) 367 | 368 | return line, char, line.start + char - 1 369 | end 370 | 371 | -- return the line represented by lineno. 372 | local function line_for_text(text, lineno) 373 | local ii = 1 374 | local i = 1 375 | local len = text:len() 376 | while ii <= len do 377 | local pos_s, pos_e = string.find(text, "([^\n]*)\n?", ii) 378 | if i == lineno then 379 | return { 380 | text = text:sub(pos_s, pos_e), 381 | start = pos_s, 382 | ["end"] = pos_e, 383 | _doc = false 384 | } 385 | end 386 | ii = pos_e + 1 387 | i = i + 1 388 | end 389 | error("not found") 390 | end 391 | 392 | local function split_path(path) 393 | local path_ids = {} 394 | 395 | local i = 1 396 | while path:find("[:.]", i) do 397 | local is, ie = path:find("[:.]", i) 398 | table.insert(path_ids, path:sub(i, is-1)) 399 | i = ie+1 400 | end 401 | table.insert(path_ids, path:sub(i, -1)) 402 | 403 | return path_ids 404 | end 405 | 406 | local definition_of 407 | --- Get pair(), and unpack them automatically 408 | -- @input t - current scope, k - current word 409 | -- @returns key, value, document 410 | local function getp(config, doc, t, k, isDefinition) 411 | -- luacheck: ignore 542 412 | local pair = t and t[k] 413 | if not pair then 414 | log.debug("no pair for %q in %_", k, t) 415 | return nil 416 | end 417 | local key, value = unpack(pair) 418 | 419 | if isDefinition then 420 | return key, value, doc 421 | end 422 | 423 | if value.tag == "Require" then 424 | -- Resolve the return value of this module 425 | local moduleName = value.module 426 | 427 | -- [NOTE] Waiting the Document entity from this function 428 | local ref = assert(analyze.module(config, moduleName)) 429 | doc = ref 430 | if ref then 431 | -- start at file scope 432 | local mt = ref.scopes and getmetatable(ref.scopes[2]) 433 | local ret = mt and mt._return and mt._return[1][1] 434 | if ret and ret.tag == "Id" then 435 | local _ 436 | _, value, doc = definition_of(config, ref, ret) 437 | else 438 | value = ret 439 | end 440 | end 441 | elseif value.tag == "String" then 442 | -- We're resolving a string as a table, this means look at the 443 | -- string metatable. This is encoded as looking for a global named 444 | -- "string", which is true in the default lua impl, but can be 445 | -- broken by crazy users doing setmetatable("", new_string), which we 446 | -- don't actually handle. 447 | key, value, doc = definition_of(config, doc, {tag="Id", "string", pos=-1}) 448 | elseif value.tag == "Call" or value.tag == "Invoke" then 449 | local v 450 | key, v, doc = definition_of(config, doc, value.ref) 451 | if v.scope then 452 | local mt = v.scope and getmetatable(v.scope) 453 | local rets = mt and mt._return or {{}} 454 | --for _, ret in ipairs(rets) do 455 | local ret = rets[1][1] 456 | -- overload. FIXME: this mutates the original which does 457 | -- not make sense if its a copy 458 | if ret.scope then 459 | for _k, _v in pairs(v.scope) do 460 | ret.scope[_k] = _v 461 | end 462 | end 463 | -- FIXME union type 464 | return key, ret, doc 465 | --end 466 | elseif v.returns then 467 | local ret = v.returns[1] 468 | if ret.type == "ref" then 469 | local _type = config.types[ret.name] 470 | return key, _type, doc 471 | end 472 | end 473 | error("call return type not understood") 474 | elseif value.tag == "Arg" then 475 | -- deref an argument 476 | -- we know it's a method call, deref as such 477 | if key[1] == "self" then 478 | end 479 | end 480 | 481 | return key, value, doc 482 | end 483 | 484 | local function nodes_to_string(id) 485 | if id.tag == "String" or id.tag == "Id" then 486 | return id[1] 487 | elseif id.tag == "Index" then 488 | assert(id[2].tag == "String") 489 | return nodes_to_string(id[1]) .. "." .. id[2][1] 490 | end 491 | end 492 | 493 | --- returns the definition of an expression AST or a position in a document 494 | -- FIXME: this is real dumb, do the expression parsing someplace else and only 495 | -- query based on AST 496 | function definition_of(config, doc, id_or_pos) 497 | local document = analyze.document(config, doc) 498 | 499 | local word, cursor 500 | if id_or_pos.tag then 501 | local id = id_or_pos 502 | if (id.tag == "String" or id.tag == "Id") then 503 | cursor = id.pos+1 504 | word = id[1] 505 | assert(type(word) == "string", require'tarantool-lsp.inspect'(id, {depth = 3})) 506 | elseif id.tag == "Index" then 507 | cursor = id.pos+1 508 | word = nodes_to_string(id) 509 | end 510 | else 511 | local line, char 512 | line, char, cursor = line_for(document, id_or_pos) 513 | local word_s = line.text:sub(1, char):find("[%w.:_]*$") 514 | local _, word_e = line.text:sub(char, -1):find("[%w_]*") 515 | word_e = word_e + char - 1 516 | word = line.text:sub(word_s, word_e) 517 | end 518 | local scope = pick_scope(document.dirty, document.scopes, cursor) 519 | local word_start = word:match("^([^.:]+)") 520 | 521 | local symbol, val, _ 522 | 523 | if word:find("[:.]") then 524 | local valdoc 525 | _, val, valdoc = getp(config, document, scope, word_start) 526 | local path_ids = split_path(word) 527 | 528 | local function follow_path(ii, _scope) 529 | if not _scope then 530 | log.fatal("not a scope: %t2", val) 531 | end 532 | local key = path_ids[ii] 533 | local last = ii == #path_ids 534 | 535 | if last then 536 | return getp(config, valdoc, _scope, key, "definition") 537 | else 538 | local _, ival, _ = getp(config, valdoc, _scope, key) 539 | 540 | if ival.tag == "Table" and ival.scope then 541 | return follow_path(ii+1, ival.scope) 542 | end 543 | end 544 | end 545 | symbol, val, document = follow_path(2, val.scope) 546 | else 547 | symbol, val, document = getp(config, document, scope, word_start, "definition") 548 | end 549 | 550 | if not symbol then 551 | return nil, nil, document 552 | end 553 | return symbol, val, document 554 | end 555 | 556 | method_handlers["textDocument/completion"] = function(config, params, id) 557 | local document = analyze.document(config, params.textDocument) 558 | if document.scopes == nil then 559 | return rpc.respond(id, { 560 | isIncomplete = false, 561 | items = {} 562 | }) 563 | end 564 | local line, char, pos = line_for(document, params.position) 565 | local word = line.text:sub(1, char-1):match("[A-Za-z_][%w_.:]*$") 566 | if not word then 567 | log.debug("%q: %_", line.text:sub(1, char-1), word) 568 | end 569 | 570 | local items = {} 571 | local used = {} 572 | local scope = pick_scope(document.dirty, document.scopes, pos) 573 | if scope == nil or word == nil then 574 | return rpc.respond(id, { 575 | isIncomplete = false, 576 | items = {} 577 | }) 578 | end 579 | log.debug("looking for %q in scope id %d", word, getmetatable(scope).id) 580 | 581 | if word:find("[:.]") then 582 | -- Divides by separator (ex.: ['box', 'cfg']) 583 | local path_ids, _ = split_path(word) 584 | -- path scope 585 | local function follow_path(ii, _scope) 586 | assert(_scope) 587 | local _iword = path_ids[ii] 588 | local last = ii == #path_ids 589 | if last then 590 | local is_method = not not word:find(":") 591 | log.debug("Is method? %_", is_method) 592 | for iname, _, val in iter_scope(_scope) do 593 | if type(iname) == "string" and iname:sub(1, _iword:len()) == _iword then 594 | local is_field = true 595 | local subitems = make_completion_items(iname, val, is_method, is_field) 596 | for _, item in ipairs(subitems) do 597 | table.insert(items, item) 598 | end 599 | end 600 | end 601 | else 602 | local node, val = getp(config, document, _scope, _iword) 603 | if node then 604 | if val and val.tag == "Table" then 605 | if val.scope then 606 | return follow_path(ii + 1, val.scope) 607 | end 608 | end 609 | end 610 | end 611 | end 612 | follow_path(1, scope) 613 | else 614 | -- variable scope 615 | for iname, node, val in iter_scope(scope) do 616 | if not used[iname] and (node.global or node.posEnd < pos) then 617 | 618 | used[iname] = true 619 | if iname:sub(1, word:len()) == word then 620 | for _, item in ipairs(make_completion_items(iname, val)) do 621 | table.insert(items, item) 622 | end 623 | end 624 | end 625 | end 626 | end 627 | 628 | -- local current_line = document.lines[params.position.line + 1]["text"] 629 | -- local left_part = current_line:sub(0, params.position.character) 630 | -- local last_token = left_part:match("[%w.:_]*$") 631 | -- if last_token then 632 | -- local raw_completions = {} 633 | -- local ADD_COMPLETION = function(cmplt) 634 | -- raw_completions[cmplt] = true 635 | -- end 636 | 637 | -- -- [?] Completion handler returns input string at the first element 638 | -- local tnt_completions = console.completion_handler(last_token, 0, last_token:len()) or {} 639 | -- local doc_completions = docs:getCompletions(last_token) 640 | -- fun.each(ADD_COMPLETION, fun.tail(tnt_completions)) 641 | -- fun.each(ADD_COMPLETION, fun.remove_if(function(cmplt) 642 | -- if raw_completions[cmplt .. '('] then 643 | -- return false 644 | -- end 645 | 646 | -- return true 647 | -- end, doc_completions)) 648 | 649 | -- for _, cmplt in fun.map(function(cmplt) return cmplt end, raw_completions) do 650 | -- local showedCmplt = cmplt 651 | -- local insertedCmplt = cmplt 652 | -- local cmpltKind = completionKinds["Field"] 653 | 654 | -- if cmplt:find("[(]") then 655 | -- cmpltKind = completionKinds["Function"] 656 | -- showedCmplt = cmplt:gsub("%(", "") 657 | -- end 658 | 659 | -- local doc = docs:get(showedCmplt) 660 | 661 | -- table.insert(items, { 662 | -- label = showedCmplt, 663 | -- kind = doc and doc.type or cmpltKind, 664 | -- insertText = insertedCmplt, 665 | -- documentation = doc and doc.description, 666 | -- detail = doc and doc.brief 667 | -- }) 668 | -- end 669 | -- end 670 | 671 | return rpc.respond(id, { 672 | isIncomplete = false, 673 | items = items 674 | }) 675 | end 676 | 677 | method_handlers["textDocument/definition"] = function(config, params, id) 678 | local document = analyze.document(config, params.textDocument) 679 | 680 | local line, char = line_for(document, params.position) 681 | local word_s = line.text:sub(1, char):find("[%w.:_]*$") 682 | local _, word_e = line.text:sub(char, -1):find("[%w_]*") 683 | word_e = word_e + char - 1 684 | local word = line.text:sub(word_s, word_e) 685 | log.debug("definition for %q", word) 686 | 687 | local symbol, _, doc2 = definition_of(config, params.textDocument, params.position) 688 | 689 | if not symbol or symbol.file == "__NONE__" then 690 | -- symbol not found 691 | log.warning("Symbol not found: %q", word) 692 | if symbol then 693 | log.warning("did find: %t", symbol) 694 | end 695 | return rpc.respond(id, json.null) 696 | end 697 | 698 | local doc = document 699 | if doc ~= doc2 then 700 | log.debug("defined in external file %q", doc2.uri) 701 | end 702 | 703 | local sub = doc2.text:sub(symbol.pos, symbol.posEnd) 704 | local word_end = word:match("([^.:]+)$") 705 | local a, b = string.find(sub, word_end, 1, true) 706 | log.debug("find %q in %q", word_end, sub) 707 | if not a then 708 | error(("failed to find %q in %q\nword from %q") 709 | :format(word_end, sub, doc2.uri)) 710 | end 711 | a, b = a + symbol.pos - 1, b + symbol.pos - 1 712 | 713 | return rpc.respond(id, { 714 | uri = doc2.uri, 715 | range = make_range(doc2, a, b) 716 | }) 717 | 718 | end 719 | 720 | method_handlers["textDocument/hover"] = function(config, params, id) 721 | local document = analyze.document(config, params.textDocument) 722 | if not document.scopes then 723 | return rpc.respondError(id, "No AST data") 724 | end 725 | 726 | local line, char, _ = line_for(document, params.position) 727 | local word_s = line.text:sub(1, char):find("[%w.:_]*$") 728 | local _, word_e = line.text:sub(char, -1):find("[%w_]*") 729 | word_e = word_e + char - 1 730 | local word = line.text:sub(word_s, word_e) 731 | log.debug("hover for %q", word) 732 | 733 | local symbol, value = definition_of(config, params.textDocument, params.position) 734 | if symbol then 735 | local contents = {} 736 | 737 | local is_field = word:find("[:.]") 738 | local is_method = word:find(":") 739 | local items = make_completion_items(word, value, is_field, is_method) 740 | 741 | if value.tag == "Function" then 742 | local item = items[1] 743 | table.insert(contents, item.label.."\n") 744 | table.insert(contents, item.documentation) 745 | end 746 | 747 | return rpc.respond(id, { 748 | contents = contents 749 | }) 750 | end 751 | 752 | -- This is the no result response, see: 753 | -- https://github.com/Microsoft/language-server-protocol/issues/261 754 | return rpc.respond(id,{ 755 | contents = {} 756 | }) 757 | end 758 | 759 | method_handlers["textDocument/documentSymbol"] = function(config, params, id) 760 | local document = analyze.document(config, params.textDocument) 761 | local symbols = {} 762 | 763 | -- FIXME: report table keys too, like a.b.c 764 | -- also consider making a linear list of symbols we can just iterate 765 | -- through 766 | for i=1, #document.scopes do 767 | local scope = document.scopes[i] 768 | for _, pair in pairs(scope) do 769 | local symbol, _ = unpack(pair) 770 | if symbol.file ~= "__NONE__" then 771 | table.insert(symbols, { 772 | name = symbol[1], 773 | kind = 13, 774 | location = make_location(document, symbol), 775 | containerName = nil -- use for fields 776 | }) 777 | end 778 | end 779 | end 780 | return rpc.respond(id, symbols) 781 | end 782 | 783 | method_handlers["textDocument/formatting"] = function(config, params, id) 784 | local doc = analyze.document(config, params.textDocument) 785 | local indent = "\t" 786 | if params.options.insertSpaces then 787 | indent = string.rep(" ", params.options.tabSize) 788 | end 789 | 790 | local format = require 'tarantool-lsp.formatting' 791 | local new = format.format(doc.text, { 792 | indent = indent, 793 | maxChars = 120, 794 | }) 795 | 796 | rpc.respond(id, { 797 | { 798 | range = make_range(doc, 1, #doc.text), 799 | newText = new 800 | } 801 | }) 802 | end 803 | 804 | method_handlers["textDocument/rangeFormatting"] = function(config, params, id) 805 | local doc = analyze.document(config, params.textDocument) 806 | local indent = "\t" 807 | if params.options.insertSpaces then 808 | indent = string.rep(" ", params.options.tabSize) 809 | end 810 | local format = require 'tarantool-lsp.formatting' 811 | local new = format.format(doc.text,{indent = indent}) 812 | 813 | local _, _, sidx = line_for(doc, params.range.start) 814 | local _, _, ridx = line_for(doc, params.range["end"]) 815 | local line, _, _ = line_for_text(new, params.range["end"].line+1) 816 | 817 | local eidx = line.start + #line.text - 1 818 | local changed = new:sub(sidx, eidx) 819 | return rpc.respond(id, { 820 | { 821 | range = make_range(doc, sidx, ridx), 822 | newText = changed 823 | } 824 | }) 825 | end 826 | 827 | method_handlers["workspace/didChangeConfiguration"] = function(config, params) 828 | assert(params.settings) 829 | merge_(config, params.settings) 830 | log.info("Config loaded, new config: %t", config) 831 | end 832 | 833 | function method_handlers.shutdown(config, _, id) 834 | config.Shutdown = true 835 | return rpc.respond(id, json.null) 836 | end 837 | 838 | function method_handlers.exit(config, _) 839 | if config.Shutdown then 840 | os.exit(0) 841 | else 842 | os.exit(1) 843 | end 844 | end 845 | 846 | return method_handlers 847 | -------------------------------------------------------------------------------- /tarantool-lsp/rpc.lua: -------------------------------------------------------------------------------- 1 | -- json-rpc implementation 2 | local json = require 'json' 3 | local rpc = {} 4 | 5 | function rpc.respond(id, result) 6 | return json.encode({ 7 | jsonrpc = "2.0", 8 | id = id or json.null, 9 | result = result 10 | }) 11 | end 12 | 13 | local lsp_error_codes = { 14 | -- Defined by json-rpc 15 | ParseError = -32700, 16 | InvalidRequest = -32600, 17 | MethodNotFound = -32601, 18 | InvalidParams = -32602, 19 | InternalError = -32603, 20 | serverErrorStart = -32099, 21 | serverErrorEnd = -32000, 22 | ServerNotInitialized = -32002, 23 | UnknownErrorCode = -32001, 24 | -- Defined by the protocol. 25 | RequestCancelled = -32800, 26 | } 27 | 28 | local valid_content_type = { 29 | ["application/vscode-jsonrpc; charset=utf-8"] = true, 30 | -- the spec says to be lenient in this case 31 | ["application/vscode-jsonrpc; charset=utf8"] = true 32 | } 33 | 34 | 35 | function rpc.respondError(id, errorMsg, errorKey, data) 36 | if not errorMsg then 37 | errorMsg = "missing error message!" 38 | end 39 | local msg = json.encode({ 40 | jsonrpc = "2.0", 41 | id = id or json.null, 42 | error = { 43 | code = lsp_error_codes[errorKey] or -32001, 44 | message = errorMsg, 45 | data = data 46 | } 47 | }) 48 | return msg 49 | end 50 | 51 | function rpc.notify(method, params) 52 | return json.encode({ 53 | jsonrpc = "2.0", 54 | method = method, 55 | params = params 56 | }) 57 | end 58 | 59 | local open_rpc = {} 60 | local next_rpc_id = 0 61 | function rpc.request(method, params, fn) 62 | local msg = json.encode({ 63 | jsonrpc = "2.0", 64 | id = next_rpc_id, 65 | method = method, 66 | params = params 67 | }) 68 | open_rpc[next_rpc_id] = fn 69 | next_rpc_id = next_rpc_id + 1 70 | return msg 71 | end 72 | 73 | function rpc.finish(data) 74 | -- response to server request 75 | local call = open_rpc[data.id] 76 | if call then 77 | call(data.result) 78 | end 79 | end 80 | 81 | return rpc 82 | -------------------------------------------------------------------------------- /tarantool-lsp/tnt-doc/completion-generator.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local log = require('tarantool-lsp.log') 3 | local utils = require('tarantool-lsp.utils') 4 | local parser = require('tarantool-lsp.tnt-doc.doc-parser') 5 | local checks = require('checks') 6 | local jp = require('jit.p') 7 | 8 | local completionKinds = { 9 | Text = 1, 10 | Method = 2, 11 | Function = 3, 12 | Constructor = 4, 13 | Field = 5, 14 | Variable = 6, 15 | Class = 7, 16 | Interface = 8, 17 | Module = 9, 18 | Property = 10, 19 | Unit = 11, 20 | Value = 12, 21 | Enum = 13, 22 | Keyword = 14, 23 | Snippet = 15, 24 | Color = 16, 25 | File = 17, 26 | Reference = 18, 27 | } 28 | 29 | local function findKindByDigit(digit) 30 | for k, v in pairs(completionKinds) do 31 | if v == digit then 32 | return k 33 | end 34 | end 35 | end 36 | 37 | local function parseDocs(doc_path) 38 | local doc_dirs = { 39 | -- box doc's 40 | { 41 | path = fio.pathjoin(doc_path, "doc", "book", "box"), 42 | name = 'box' 43 | }, 44 | -- libraries docs's + some box modules 45 | { 46 | path = fio.pathjoin(doc_path, "doc", "reference", "reference_lua"), 47 | name = 'libraries', 48 | separate = true 49 | } 50 | } 51 | 52 | local function basename(name) 53 | return name:match("^(.*)%.rst$") 54 | end 55 | 56 | local terms = {} 57 | 58 | for _, doc in ipairs(doc_dirs) do 59 | local work_dir = doc.path 60 | local docs = fio.glob(fio.pathjoin(work_dir, "*.rst")) 61 | 62 | terms[doc.name] = {} 63 | local termsTable = terms[doc.name] 64 | 65 | for _, doc_file in ipairs(docs) do 66 | local moduleName = basename(fio.basename(doc_file)) 67 | 68 | if doc.separate then 69 | terms[doc.name][moduleName] = {} 70 | termsTable = terms[doc.name][moduleName] 71 | end 72 | 73 | local f = fio.open(doc_file, { 'O_RDONLY' }) 74 | local text = f:read() 75 | local ok, trace = xpcall(parser.parseDocFile, debug.traceback, text, termsTable) 76 | 77 | f:close() 78 | if not ok then 79 | log.error("Error parse %s file. Traceback: %s. Exit...", trace, doc_file) 80 | os.exit(1) 81 | end 82 | end 83 | end 84 | 85 | return terms 86 | end 87 | 88 | local function generateCompletionFile(tnt_module) 89 | local output = { 90 | type = 'table', 91 | fields = {}, 92 | } 93 | 94 | for k, v in pairs(tnt_module) do 95 | local termType = findKindByDigit(v.type):lower() 96 | output.fields[k] = { 97 | type = termType, 98 | argsDisplay = v.argsDisplay, 99 | detail = v.brief, 100 | description = v.description, 101 | } 102 | end 103 | 104 | return output 105 | end 106 | 107 | local function generateCompletions(opts) 108 | checks({ 109 | bootstrap_profiler = '?string', 110 | doc_dir = 'string', 111 | completion_dir = 'string' 112 | }) 113 | 114 | opts = opts or {} 115 | if opts.bootstrap_profiler then 116 | jp.start('f', '/tmp/tnt-lsp-doc-profile.txt') 117 | end 118 | 119 | local terms = parseDocs(opts.doc_dir) 120 | 121 | jp.stop() 122 | 123 | if not fio.path.exists(opts.completion_dir) then 124 | fio.mkdir(opts.completion_dir) 125 | end 126 | 127 | local serpent = require('serpent') 128 | for name, library in pairs(terms.libraries) do 129 | local result = { 130 | type = "table", 131 | fields = {} 132 | } 133 | result.fields[name] = generateCompletionFile(library) 134 | 135 | local f = io.open(opts.completion_dir .. '/' .. name .. '.lua', 'w') 136 | f:write("return ", serpent.block(result, {comment = false})) 137 | f:close() 138 | end 139 | end 140 | 141 | return { 142 | generate = generateCompletions 143 | } 144 | -------------------------------------------------------------------------------- /tarantool-lsp/tnt-doc/doc-manager.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local log = require('tarantool-lsp.log') 3 | local utils = require('tarantool-lsp.utils') 4 | local completion_generator = require('tarantool-lsp.tnt-doc.completion-generator') 5 | local checks = require('checks') 6 | 7 | local DocumentationManager = {} 8 | 9 | function DocumentationManager.initDoc(opts) 10 | checks({ 11 | doc_dir = 'string', 12 | completion_dir = 'string' 13 | }) 14 | 15 | local git = utils.which('git') 16 | if git == nil then 17 | return nil, "Error! You need to install 'git' for docs initializing." 18 | end 19 | 20 | if not fio.path.exists(opts.doc_dir) then 21 | local ok, err = fio.mkdir(opts.doc_dir, tonumber(777, 8)) 22 | if not ok then 23 | return nil, ("Can't create directory %s for doc: %s").format(opts.doc_dir, err) 24 | end 25 | end 26 | 27 | local res, err = utils.exec_call("cd %q && %s clone %s .", opts.doc_dir, git, "https://github.com/tarantool/doc.git") 28 | if not res then 29 | return nil, err 30 | end 31 | 32 | completion_generator.generate(opts) 33 | 34 | return true 35 | end 36 | 37 | function DocumentationManager.updateDoc(opts) 38 | checks({ 39 | doc_dir = 'string', 40 | completion_dir = 'string' 41 | }) 42 | 43 | local git = utils.which('git') 44 | if git == nil then 45 | return nil, "Error! You need to install 'git' for docs updating." 46 | end 47 | 48 | local res, err = utils.exec_call("cd %q && %s pull", opts.doc_dir, git) 49 | if not res then 50 | return nil, err 51 | end 52 | 53 | completion_generator.generate(opts) 54 | 55 | return true 56 | end 57 | 58 | function DocumentationManager:init(opts) 59 | checks('table', { completions_dir = 'string' }) 60 | 61 | if not fio.path.exists(opts.completions_dir) then 62 | return nil, "Completion directory isn't exist" 63 | end 64 | 65 | local libraries = {} 66 | 67 | local cmpltFiles = fio.glob(fio.pathjoin(opts.completions_dir, '*.lua')) 68 | for _, libname in ipairs(cmpltFiles) do 69 | libname = fio.basename(libname) 70 | libname = libname:gsub("%.lua", "") 71 | libraries[libname] = true 72 | end 73 | 74 | return libraries 75 | end 76 | 77 | return DocumentationManager 78 | -------------------------------------------------------------------------------- /tarantool-lsp/tnt-doc/doc-parser.lua: -------------------------------------------------------------------------------- 1 | local log = require('tarantool-lsp.log') 2 | 3 | local function ltrim(s) 4 | return (s:gsub("^%s*", "")) 5 | end 6 | 7 | local function rtrim(s) 8 | local n = #s 9 | while n > 0 and s:find("^%s", n) do n = n - 1 end 10 | return s:sub(1, n) 11 | end 12 | 13 | local completionKinds = { 14 | Text = 1, 15 | Method = 2, 16 | Function = 3, 17 | Constructor = 4, 18 | Field = 5, 19 | Variable = 6, 20 | Class = 7, 21 | Interface = 8, 22 | Module = 9, 23 | Property = 10, 24 | Unit = 11, 25 | Value = 12, 26 | Enum = 13, 27 | Keyword = 14, 28 | Snippet = 15, 29 | Color = 16, 30 | File = 17, 31 | Reference = 18, 32 | } 33 | 34 | local function parseFunction(scope, moduleName) 35 | local is, ie, funcName = scope:find("^([%w.:_]+)") 36 | local argsDisplay 37 | argsDisplay, scope = scope:match("%(([^\n]*)%)\n\n(.*)", ie) 38 | if not scope then 39 | return nil 40 | end 41 | 42 | -- NOTE: Scope based regexp 43 | local termDescription = scope:match("^(.*)\n%s*\n%s*%:%w+[%s%w%-]*%:") 44 | -- Temporally solution 45 | if not termDescription then 46 | termDescription = scope 47 | end 48 | termDescription = rtrim(ltrim(termDescription or "")) 49 | 50 | return { name = funcName, description = termDescription, type = completionKinds['Function'], 51 | argsDisplay = argsDisplay } 52 | end 53 | 54 | local function parseIndex(scope, moduleName) 55 | local is, ie = scope:find("%+[%=]+%+[%=]+%+\n") 56 | local index_rows = scope:sub(ie + 1, scope:len()) 57 | local index = {} 58 | 59 | local ROW_SEPARATOR = "%+[%-]+%+[%-]+%+\n" 60 | local TEXT_REGEXP = "[%w.,:()%s'`+/-]+" 61 | local FUNC_REGEXP = "[%w._()]+" 62 | local ROW_REGEXP = "[%s]*%|[%s]*%:ref%:%`(" .. FUNC_REGEXP .. ")" 63 | 64 | local i = 1 65 | while true do 66 | local is, ie, func_name = index_rows:find(ROW_REGEXP, i) 67 | if not is then 68 | break 69 | end 70 | 71 | if moduleName and func_name:find(moduleName .. '.') == 1 then 72 | func_name = func_name:sub(moduleName:len() + 2) 73 | end 74 | 75 | local row_dump = index_rows:sub(ie + 1, index_rows:find(ROW_SEPARATOR, ie + 1)) 76 | 77 | local desc = "" 78 | for desc_row in row_dump:gmatch("[^\n][%s]*%|[%s]*(" .. TEXT_REGEXP .. ")%|\n") do 79 | desc = desc .. desc_row 80 | end 81 | 82 | index[func_name:gsub("%(%)", "")] = rtrim(desc):gsub("[%s]+", " ") 83 | i = ie + 1 84 | end 85 | 86 | return index 87 | end 88 | 89 | local function findNextTerm(text, pos, termType) 90 | local directives = { 91 | { pattern = "%.%. module%:%:", name = "module" }, 92 | { pattern = "%.%. function%:%:", name = "function" }, 93 | } 94 | 95 | local headings = { 96 | { pattern = "[%-]+\n[%s]+Submodule%s", name = "submodule" }, 97 | { pattern = "[%=]+\n[%s]+Overview[%s]*\n[%=]+\n\n", name = "overview" }, 98 | { pattern = "[%=]+\n[%s]+Index[%s]*\n[%=]+\n\n", name = "index" } 99 | } 100 | 101 | local terms = directives 102 | if termType == 'headings' then 103 | terms = headings 104 | end 105 | 106 | local nextTerm 107 | for _, term in ipairs(terms) do 108 | local is, ie = text:find(term.pattern, pos) 109 | if is then 110 | if not nextTerm then 111 | nextTerm = { pos = is, term = term, e_pos = ie } 112 | else 113 | if is < nextTerm.pos then 114 | nextTerm = { pos = is, term = term, e_pos = ie } 115 | end 116 | end 117 | end 118 | end 119 | 120 | return nextTerm 121 | end 122 | 123 | local function normalizeToMarkDown(text) 124 | if not text then 125 | return nil 126 | end 127 | 128 | local REF_REGEXP = "%:ref%:`([^<]+)%s[^>]+%>%`" 129 | -- Normalize references 130 | while text:match(REF_REGEXP) do 131 | text = text:gsub(REF_REGEXP, "`%1`") 132 | end 133 | 134 | local CODEBLOCK_REGEXP = "%.%. code%-block%:%:%s([^\n]*)\n\n(.-)\n\n" 135 | while true do 136 | local language, code = text:match(CODEBLOCK_REGEXP) 137 | if not code then 138 | break 139 | end 140 | 141 | -- `tarantoolsession` is not supported MD code language 142 | language = language == 'tarantoolsession' and 'lua' or language:lower() 143 | text = text:gsub(CODEBLOCK_REGEXP, "```" .. language .. "\n%2\n```\n\n") 144 | end 145 | 146 | return text 147 | end 148 | 149 | local function truncateScope(text, startPos) 150 | local nextTerm = findNextTerm(text, startPos) 151 | local lastPos = text:len() 152 | if nextTerm then 153 | lastPos = nextTerm.pos 154 | end 155 | 156 | return text:sub(startPos, lastPos - 1) 157 | end 158 | 159 | local function create_if_not_exist(terms, termName, data) 160 | local existenTerm = terms[termName] 161 | if not existenTerm then 162 | terms[termName] = data 163 | existenTerm = terms[termName] 164 | 165 | existenTerm.name = termName 166 | end 167 | 168 | if not existenTerm.description then 169 | existenTerm.description = data.description 170 | end 171 | if not existenTerm.brief then 172 | existenTerm.brief = data.brief 173 | end 174 | existenTerm.description = normalizeToMarkDown(existenTerm.description) 175 | 176 | return existenTerm 177 | end 178 | 179 | local it = 1 180 | 181 | local function parseHeadings(titleName, text, terms) 182 | local i = 1 183 | -- Scope for functions and other objects 184 | local moduleName 185 | 186 | while true do 187 | local nextTerm = findNextTerm(text, i, 'headings') 188 | if not nextTerm then 189 | break 190 | end 191 | 192 | if nextTerm.term.name == "submodule" then 193 | moduleName = text:match("%`([%w.]+)%`", nextTerm.pos) 194 | create_if_not_exist(terms, moduleName, { type = completionKinds['Module'] }) 195 | elseif nextTerm.term.name == "overview" then 196 | -- TODO: Uncomment 197 | -- assert(moduleName ~= nil, "Module name should be setted") 198 | if moduleName then 199 | create_if_not_exist(terms, moduleName, { description = truncateScope(text, nextTerm.e_pos) }) 200 | end 201 | elseif nextTerm.term.name == "index" then 202 | local index = parseIndex(truncateScope(text, nextTerm.e_pos), titleName) 203 | for func, brief_desc in pairs(index) do 204 | -- TODO: Maybe it's not a function... 205 | create_if_not_exist(terms, func, { brief = brief_desc, type = completionKinds['Function'] }) 206 | end 207 | end 208 | 209 | i = nextTerm.e_pos + 1 210 | end 211 | end 212 | 213 | -- Parse only functions 214 | local function parseDocFile(text, terms) 215 | 216 | 217 | -- Scope for functions and other objects 218 | local moduleName 219 | 220 | local i = 1 221 | -- local terms = {} 222 | while true do 223 | local nextTerm = findNextTerm(text, i) 224 | if not nextTerm then 225 | break 226 | end 227 | 228 | if nextTerm.term.name == "module" then 229 | local is, ie, mName = text:find("%.%. module%:%: ([%w.:_]*)\n", nextTerm.pos) 230 | moduleName = mName 231 | local _ = create_if_not_exist(terms, moduleName, { type = completionKinds['Module'], 232 | description = truncateScope(text, ie + 1) }) 233 | elseif nextTerm.term.name == "function" then 234 | local is, ie = text:find("%.%. function%:%:", nextTerm.pos) 235 | 236 | local nextNextTerm = findNextTerm(text, nextTerm.pos + 1) 237 | -- Skip space between directive and term name 238 | local scope = text:sub(ie + 2, nextNextTerm and nextNextTerm.pos or text:len()) 239 | local term = parseFunction(scope, moduleName) 240 | 241 | if term then 242 | create_if_not_exist(terms, term.name, term) 243 | end 244 | end 245 | 246 | i = nextTerm.e_pos + 1 247 | end 248 | 249 | parseHeadings(moduleName, text, terms) 250 | end 251 | 252 | return { 253 | parseDocFile = parseDocFile 254 | } 255 | -------------------------------------------------------------------------------- /tarantool-lsp/unicode.lua: -------------------------------------------------------------------------------- 1 | -- unicode-based utilities 2 | -- these are hard-adapted to the needs of lua-lsp, sorry 3 | -- 4 | 5 | local unicode = {} 6 | 7 | local shift_6 = 2^6 8 | local shift_12 = 2^12 9 | local shift_18 = 2^18 10 | -- The iterator is from the lua 5.3 reference manual: 11 | -- "[\0-\x7F\xC2-\xF4][\x80-\xBF]*" 12 | -- modifications are to keep 5.1-3 compat. 13 | local patt 14 | if setfenv then -- lua5.1 compat 15 | patt = "[%z\01-\127\194-\244][\128-\191]*" 16 | else 17 | patt = "[\0-\127\194-\244][\128-\191]*" 18 | end 19 | 20 | --- given a utf8 string, and a utf16 code unit index (0-indexed), find the 21 | -- equivalent byte index (1-indexed) 22 | function unicode.to_bytes(str, utf16) 23 | local byte_i = 0 24 | local codeunit_i = -1 25 | 26 | assert(utf16 >= 0, "indexes start at 0") 27 | if utf16 == 0 then return 1 end -- special case, empty strings 28 | 29 | -- this is from https://github.com/Stepets/utf8.lua/blob/master/utf8.lua 30 | local codepoint, bytes = 0, 1 31 | for byte_seq in str:gmatch(patt) do 32 | if codepoint >= 0x010000 then 33 | -- 2 code units 34 | codeunit_i = codeunit_i + 2 35 | else 36 | codeunit_i = codeunit_i + 1 37 | end 38 | 39 | byte_i = byte_i + bytes 40 | if codeunit_i == utf16 then 41 | return byte_i 42 | end 43 | 44 | bytes = byte_seq:len() 45 | if bytes == 1 then 46 | codepoint = byte_seq:byte(1) 47 | elseif bytes == 2 then 48 | local byte0,byte1 = byte_seq:byte(1,2) 49 | local code0,code1 = byte0-0xC0,byte1-0x80 50 | codepoint = code0*shift_6 + code1 51 | elseif bytes == 3 then 52 | local byte0,byte1,byte2 = byte_seq:byte(1,3) 53 | local code0,code1,code2 = byte0-0xE0,byte1-0x80,byte2-0x80 54 | codepoint = code0*shift_12 + code1*shift_6 + code2 55 | elseif bytes == 4 then 56 | local b0,b1,b2,b3 = byte_seq:byte(1,4) 57 | local code0,code1,code2,code3 = b0-0xF0,b1-0x80,b2-0x80,b3-0x80 58 | codepoint = code0*shift_18 + code1*shift_12 + code2*shift_6 + code3 59 | end 60 | end 61 | 62 | if codepoint >= 0x010000 then 63 | -- 2 code units 64 | codeunit_i = codeunit_i + 2 65 | else 66 | codeunit_i = codeunit_i + 1 67 | end 68 | 69 | byte_i = byte_i + bytes 70 | if codeunit_i == utf16 then 71 | return byte_i 72 | end 73 | error("invalid index") 74 | end 75 | 76 | -- note: byte_index is 1-indexed, return value is 0-indexed 77 | function unicode.to_codeunits(str, byte_index) 78 | local byte_i = 0 79 | local codeunit_i = -1 80 | 81 | assert(byte_index > 0, "indexes start at 1") 82 | if byte_index == 1 then return 0 end -- special case, empty strings 83 | 84 | -- this is from https://github.com/Stepets/utf8.lua/blob/master/utf8.lua 85 | local codepoint, bytes = 0, 1 86 | for byte_seq in str:gmatch(patt) do 87 | if codepoint >= 0x010000 then 88 | -- 2 code units 89 | codeunit_i = codeunit_i + 2 90 | else 91 | codeunit_i = codeunit_i + 1 92 | end 93 | 94 | byte_i = byte_i + bytes 95 | if byte_i == byte_index then 96 | return codeunit_i 97 | end 98 | 99 | bytes = byte_seq:len() 100 | if bytes == 1 then 101 | codepoint = byte_seq:byte(1) 102 | elseif bytes == 2 then 103 | local byte0,byte1 = byte_seq:byte(1,2) 104 | local code0,code1 = byte0-0xC0,byte1-0x80 105 | codepoint = code0*shift_6 + code1 106 | elseif bytes == 3 then 107 | local byte0,byte1,byte2 = byte_seq:byte(1,3) 108 | local code0,code1,code2 = byte0-0xE0,byte1-0x80,byte2-0x80 109 | codepoint = code0*shift_12 + code1*shift_6 + code2 110 | elseif bytes == 4 then 111 | local b0,b1,b2,b3 = byte_seq:byte(1,4) 112 | local code0,code1,code2,code3 = b0-0xF0,b1-0x80,b2-0x80,b3-0x80 113 | codepoint = code0*shift_18 + code1*shift_12 + code2*shift_6 + code3 114 | end 115 | end 116 | 117 | if codepoint >= 0x010000 then 118 | -- 2 code units 119 | codeunit_i = codeunit_i + 2 120 | else 121 | codeunit_i = codeunit_i + 1 122 | end 123 | 124 | byte_i = byte_i + bytes 125 | if byte_i == byte_index then 126 | return codeunit_i 127 | end 128 | error("invalid index") 129 | end 130 | 131 | return unicode 132 | -------------------------------------------------------------------------------- /tarantool-lsp/utils.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | 3 | local function is_executable(path) 4 | local S_IEXEC = 64 5 | return bit.band(fio.stat(path).mode, S_IEXEC) ~= 0 6 | end 7 | 8 | local function which(binary) 9 | for _, path in ipairs(string.split(os.getenv("PATH"), ':') or {}) do 10 | for _, file in ipairs(fio.listdir(path) or {}) do 11 | local full_path = fio.pathjoin(path, file) 12 | if file == binary and 13 | fio.path.exists(full_path) and 14 | fio.path.is_file(full_path) and 15 | is_executable(full_path) then 16 | return full_path 17 | end 18 | end 19 | end 20 | end 21 | 22 | local function call(command, ...) 23 | local res, err = io.popen(string.format(command, ...)) 24 | 25 | if res == nil then 26 | return nil, ("Failed to execute '%s': %s").format(command, err) 27 | end 28 | 29 | local result = res:read("*all") 30 | return result 31 | end 32 | 33 | return { 34 | exec_call = call, 35 | which = which 36 | } 37 | -------------------------------------------------------------------------------- /tarantool-lsp/websocket-lib.lua: -------------------------------------------------------------------------------- 1 | local rpc = require 'tarantool-lsp.rpc' 2 | local method_handlers = require 'tarantool-lsp.methods' 3 | local ws = require 'websocket' 4 | local json = require 'json' 5 | local docs = require 'tarantool-lsp.tnt-doc.doc-manager' 6 | 7 | local handshake = require('websocket.handshake') 8 | local fio = require('fio') 9 | local log = require('log') 10 | 11 | local DETACHED = 101 12 | 13 | -- [path] = libraries 14 | local completions_cache = {} 15 | 16 | local create_handler = function(options) 17 | local path = debug.getinfo(1).source:match("@?(.*/)") 18 | 19 | log.info('Path: %s', path) 20 | 21 | local completion_dir = options and options.completion_root or fio.pathjoin(path, 'completions') 22 | if not completions_cache[completion_dir] then 23 | local libraries, err = docs:init({ 24 | completions_dir =completion_dir 25 | }) 26 | if err ~= nil then 27 | log.info("Docs subsystem error: %s", err) 28 | error(err) 29 | end 30 | completions_cache[completion_dir] = libraries 31 | end 32 | return function(req) 33 | local validated, errresp = handshake.validate_request(req) 34 | 35 | if validated then 36 | --log.info(req) 37 | req.s:write( 38 | handshake.reduce_response( 39 | handshake.accept_upgrade(req))) 40 | local ws_peer = ws.new(req.s, 10, false, true, req) 41 | local config = { 42 | language = "5.1", 43 | builtins = {"5_1"}, 44 | packagePath = {"./?.lua"}, 45 | debugMode = false, 46 | documents = {}, 47 | types = {}, 48 | web_server = true, 49 | libraries = completions_cache[completion_dir], 50 | _useNativeLuacheck = false -- underscore means "experimental" here 51 | } 52 | while true do 53 | local message, err = ws_peer:read() 54 | if not message or message.opcode == nil then 55 | break 56 | end 57 | local data = json.decode(message.data) 58 | if data.method then 59 | -- request 60 | if data.method == 'exit' then 61 | return DETACHED 62 | end 63 | if not method_handlers[data.method] then 64 | -- log.verbose("confused by %t", data) 65 | err = string.format("%q: Not found/NYI", tostring(data.method)) 66 | if data.id then 67 | rpc.respondError(data.id, err, "MethodNotFound") 68 | else 69 | --log.warning("%s", err) 70 | end 71 | else 72 | local ok 73 | ok, err = xpcall(function() 74 | local response = method_handlers[data.method](config, data.params, data.id) 75 | if response then 76 | ws_peer:write(response) 77 | end 78 | end, debug.traceback) 79 | if not ok then 80 | if data.id then 81 | local msgError = rpc.respondError(data.id, err, "InternalError") 82 | ws_peer:write(msgError) 83 | else 84 | --local msgError = rpc.respondError(1, err, "InternalError") 85 | --ws_peer:write(msgError) 86 | log.info("%s", tostring(err)) 87 | end 88 | end 89 | end 90 | elseif data.result then 91 | rpc.finish(data) 92 | elseif data.error then 93 | log("client error:%s", data.error.message) 94 | end 95 | end 96 | return DETACHED 97 | else 98 | return {status=400, body="No way"} 99 | end 100 | end 101 | end 102 | 103 | return { 104 | create_handler = create_handler, 105 | } 106 | -------------------------------------------------------------------------------- /tarantool-lsp/websocket.lua: -------------------------------------------------------------------------------- 1 | local rpc = require 'tarantool-lsp.rpc' 2 | local log = require 'tarantool-lsp.log' 3 | local method_handlers = require 'tarantool-lsp.methods' 4 | local ws = require 'websocket' 5 | local json = require 'json' 6 | local docs = require 'tarantool-lsp.tnt-doc.doc-manager' 7 | 8 | 9 | _G.Types = _G.Types or {} 10 | _G.Documents = _G.Documents or {} 11 | _G.Globals = _G.Globals -- defined in analyze.lua 12 | -- selfish default 13 | local config = { 14 | language = "5.1", 15 | builtins = {"5_1"}, 16 | packagePath = {"./?.lua"}, 17 | documents = {}, 18 | debugMode = false, 19 | _useNativeLuacheck = false -- underscore means "experimental" here 20 | } 21 | _G.Shutdown = false 22 | _G.Initialized = _G.Initialized or false 23 | _G.print = function() 24 | error("illegal print, use log() instead:", 2) 25 | end 26 | 27 | local function main(_) 28 | local ok, err = docs:init({ completions_dir = fio.pathjoin(_G._ROOT_PATH, 'completions') }) 29 | local port = os.getenv('PORT') and tonumber(os.getenv('PORT')) or 8080 30 | 31 | ws.server('ws://0.0.0.0:' .. port, function (ws_peer) 32 | while true do 33 | local message, err = ws_peer:read() 34 | if not message or message.opcode == nil then 35 | break 36 | end 37 | local data = json.decode(message.data) 38 | if data.method then 39 | -- request 40 | if not method_handlers[data.method] then 41 | -- log.verbose("confused by %t", data) 42 | err = string.format("%q: Not found/NYI", tostring(data.method)) 43 | if data.id then 44 | rpc.respondError(data.id, err, "MethodNotFound") 45 | else 46 | log.warning("%s", err) 47 | end 48 | else 49 | local ok 50 | ok, err = xpcall(function() 51 | local response = method_handlers[data.method](config, data.params, data.id) 52 | ws_peer:write(response) 53 | end, debug.traceback) 54 | if not ok then 55 | if data.id then 56 | local msgError = rpc.respondError(data.id, err, "InternalError") 57 | ws_peer:write(msgError) 58 | else 59 | log.warning("%s", tostring(err)) 60 | end 61 | end 62 | end 63 | elseif data.result then 64 | rpc.finish(data) 65 | elseif data.error then 66 | log("client error:%s", data.error.message) 67 | end 68 | end 69 | end) 70 | log.verbose('start weboscket') 71 | 72 | end 73 | 74 | return main 75 | --https://code.visualstudio.com/blogs/2016/06/27/common-language-protocol 76 | -------------------------------------------------------------------------------- /tarantoollsp.rb: -------------------------------------------------------------------------------- 1 | class Tarantoollsp < Formula 2 | desc "LSP server for Tarantool/Lua based codewriters" 3 | homepage "https://github.com/tarantool/lua-lsp" 4 | url "https://download.tarantool.org/tarantool/2.2/src/tarantool-2.2.1.1.tar.gz" 5 | sha256 "42c6c61b7d9a2444afd96e4f5e1828da18ea2637d1e9d61dc543436ae48dd87f" 6 | head "https://github.com/tarantool/lua-lsp.git", :branch => "master" 7 | 8 | # By default, Tarantool from brew includes devel headers 9 | depends_on "tarantool" 10 | 11 | def install 12 | system "tarantoolctl", "rocks", "make" 13 | prefix.install "tarantool-lsp", ".rocks", "bin" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/completions/golden_files/csv.lua: -------------------------------------------------------------------------------- 1 | return { 2 | fields = { 3 | csv = { 4 | fields = { 5 | csv = { 6 | args = false, 7 | type = "module" 8 | }, 9 | dump = { 10 | args = { 11 | {} 12 | }, 13 | type = "function" 14 | }, 15 | iterate = { 16 | args = { 17 | {} 18 | }, 19 | type = "function" 20 | }, 21 | load = { 22 | args = { 23 | {} 24 | }, 25 | type = "function" 26 | } 27 | }, 28 | type = "table" 29 | } 30 | }, 31 | type = "table" 32 | } 33 | -------------------------------------------------------------------------------- /test/completions/golden_files/json.lua: -------------------------------------------------------------------------------- 1 | return { 2 | fields = { 3 | json = { 4 | fields = { 5 | decode = { 6 | args = { 7 | {} 8 | }, 9 | type = "function" 10 | }, 11 | encode = { 12 | args = { 13 | {} 14 | }, 15 | type = "function" 16 | }, 17 | }, 18 | type = "table" 19 | } 20 | }, 21 | type = "table" 22 | } 23 | -------------------------------------------------------------------------------- /test/completions/libs_completion_test.lua: -------------------------------------------------------------------------------- 1 | local mock_loop = require('spec.mock_loop') 2 | local test = require('luatest') 3 | 4 | local DEFAULT_CSV_ITEMS = { 5 | {insertText="dump()", label="dump() ", kind=2,}, 6 | {insertText="load()", label="load() ", kind=2,}, 7 | {insertText="iterate()", label="iterate() ", kind=2} 8 | } 9 | 10 | local DEFAULT_JSON_ITEMS = { 11 | {insertText="decode()", kind=2, label="decode() "}, 12 | {insertText="encode()", kind=2, label="encode() "} 13 | } 14 | 15 | package.loaded['tarantool-lsp.tnt-doc.doc-manager'] = { 16 | init = function() end, 17 | getInternalLibrariesList = function() 18 | return { 19 | csv = true, 20 | json = true 21 | } 22 | end 23 | } 24 | 25 | package.loaded['tarantool-lsp.completions.csv'] = require('test.completions.golden_files.csv') 26 | package.loaded['tarantool-lsp.completions.json'] = require('test.completions.golden_files.json') 27 | 28 | local function make_completion_call(before, after, position) 29 | local resp 30 | 31 | mock_loop(function(rpc) 32 | local doc = { 33 | uri = "file:///tmp/fake.lua" 34 | } 35 | rpc.notify("textDocument/didOpen", { 36 | textDocument = {uri = doc.uri, text = before} 37 | }) 38 | rpc.notify("textDocument/didChange", { 39 | textDocument = {uri = doc.uri, text = after}, 40 | contentChanges = {} 41 | }) 42 | rpc.request("textDocument/completion", { 43 | textDocument = doc, 44 | position = position 45 | }, function(out) 46 | resp = out 47 | end) 48 | end) 49 | 50 | return resp 51 | end 52 | 53 | local libs_completion = test.group("libs-completion") 54 | 55 | libs_completion.test_basic = function() 56 | local text = "local csv = require('csv')\n" 57 | 58 | local cmplt = make_completion_call(text, text .. "csv.", {line = 1, character = 4}) 59 | test.assert_equals(cmplt, { 60 | isIncomplete = false, 61 | items = DEFAULT_CSV_ITEMS, 62 | }) 63 | end 64 | 65 | -- The reason of the test is 'dirty scopes'. Doesn't take account of right border of the scopes. 66 | -- In case of file extending, we have updated right borders, which we can update manually without AST data. 67 | --[[ 68 | Case 1: Document was extended, current scopes are valid and new text righter that scope borders 69 | Case 2: Document was extended, current scopes are valid but new text placed on the any previous scope 70 | ```lua 71 | do 72 | local m = require('...') 73 | end 74 | 75 | c|sv. 76 | ``` 77 | 78 | where | - is the right border 79 | ]] 80 | -- libs_completion.test_closed_scope = function() 81 | -- local text = 82 | -- [[ 83 | -- do 84 | -- local csv = require('csv') 85 | -- end 86 | 87 | -- ]] 88 | 89 | -- local cmpltText = 90 | -- [[ 91 | -- do 92 | -- local csv = require('csv') 93 | -- end 94 | 95 | -- csv. 96 | -- ]] 97 | -- local cmplt = make_completion_call(text, cmpltText, {line = 4, character = 4}) 98 | -- test.assertEquals(cmplt, { isIncomplete = false, items = {} }) 99 | -- end 100 | 101 | libs_completion.test_global_scope = function() 102 | local text = 103 | [[ 104 | local csv = require('csv') 105 | ]] 106 | 107 | local cmpltText = 108 | [[ 109 | local csv = require('csv') 110 | 111 | local function abc() 112 | csv. 113 | end 114 | ]] 115 | local cmplt = make_completion_call(text, cmpltText, {line = 3, character = 6}) 116 | test.assert_equals(cmplt, { 117 | isIncomplete = false, 118 | items = DEFAULT_CSV_ITEMS 119 | }) 120 | end 121 | 122 | libs_completion.test_local_scope = function() 123 | local text = 124 | [[ 125 | local function abc() 126 | local csv = require('csv') 127 | end 128 | ]] 129 | 130 | local cmpltText = 131 | [[ 132 | local function abc() 133 | local csv = require('csv') 134 | csv. 135 | end 136 | ]] 137 | 138 | local cmplt = make_completion_call(text, cmpltText, {line = 2, character = 6}) 139 | test.assert_equals(cmplt, { 140 | isIncomplete = false, 141 | items = DEFAULT_CSV_ITEMS 142 | }) 143 | end 144 | 145 | libs_completion.test_variable_shadowing = function() 146 | local text = 147 | [[ 148 | local lib = require('csv') 149 | 150 | do 151 | local lib = require('json') 152 | end 153 | ]] 154 | 155 | local cmpltText = 156 | [[ 157 | local lib = require('csv') 158 | 159 | do 160 | local lib = require('json') 161 | lib. 162 | end 163 | ]] 164 | local cmplt = make_completion_call(text, cmpltText, {line = 4, character = 6}) 165 | test.assert_equals(cmplt, { 166 | isIncomplete = false, 167 | items = DEFAULT_JSON_ITEMS 168 | }) 169 | end 170 | 171 | -- TODO: Reassignment to another variable 172 | --------------------------------------------------------------------------------