├── .gitignore ├── README.md ├── nelua-lsp.lua └── nelua-lsp ├── json.lua ├── parseerror.lua ├── server.lua └── utils.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Nelua LSP 2 | ========= 3 | 4 | Features: 5 | * Diagnostics 6 | * Hover 7 | * Code completion 8 | * Go to definition 9 | 10 | ## Usage 11 | 12 | launch command: 13 | ``` 14 | nelua --script path/to/nelua-lsp.lua 15 | ``` 16 | -------------------------------------------------------------------------------- /nelua-lsp.lua: -------------------------------------------------------------------------------- 1 | local lfs = require 'lfs' 2 | local console = require 'nelua.utils.console' 3 | local except = require 'nelua.utils.except' 4 | local fs = require 'nelua.utils.fs' 5 | local sstream = require 'nelua.utils.sstream' 6 | local analyzer = require 'nelua.analyzer' 7 | local aster = require 'nelua.aster' 8 | local typedefs = require 'nelua.typedefs' 9 | local AnalyzerContext = require 'nelua.analyzercontext' 10 | local generator = require 'nelua.cgenerator' 11 | local inspect = require 'nelua.thirdparty.inspect' 12 | local spairs = require 'nelua.utils.iterators'.spairs 13 | 14 | local server = require 'nelua-lsp.server' 15 | local json = require 'nelua-lsp.json' 16 | local parseerror = require 'nelua-lsp.parseerror' 17 | local utils = require 'nelua-lsp.utils' 18 | 19 | local stdout 20 | do 21 | -- Fix CRLF problem on windows 22 | lfs.setmode(io.stdin, 'binary') 23 | lfs.setmode(io.stdout, 'binary') 24 | lfs.setmode(io.stderr, 'binary') 25 | -- Redirect stderr/stdout to a file so we can debug errors. 26 | local err = io.stderr 27 | stdout = io.stdout 28 | _G.io.stdout, _G.io.stderr = err, err 29 | _G.print = console.debug 30 | _G.printf = console.debugf 31 | end 32 | 33 | local astcache = {} 34 | local codecache = {} 35 | 36 | local function map_severity(text) 37 | if text == 'error' or text == 'syntax error' then return 1 end 38 | if text == 'warning' then return 2 end 39 | if text == 'info' then return 3 end 40 | return 4 41 | end 42 | 43 | local function analyze_ast(input, infile, uri, skip) 44 | local ast 45 | local ok, err = except.trycall(function() 46 | ast = aster.parse(input, infile) 47 | local context = AnalyzerContext(analyzer.visitors, ast, generator) 48 | except.try(function() 49 | if not server.root_path then 50 | local dir = infile:match('(.+)'..utils.dirsep) 51 | lfs.chdir(dir) 52 | end 53 | for k, v in pairs(typedefs.primtypes) do 54 | if v.metafields then 55 | v.metafields = {} 56 | end 57 | end 58 | context = analyzer.analyze(context) 59 | end, function(e) 60 | -- todo 61 | end) 62 | end) 63 | local diagnostics = {} 64 | if not ok then 65 | if err.message then 66 | local stru = parseerror(err.message) 67 | for _, ins in ipairs(stru) do 68 | table.insert(diagnostics, { 69 | range = { 70 | ['start'] = {line = ins.line - 1, character = ins.character - 1}, 71 | ['end'] = {line = ins.line - 1, character = ins.character + ins.length - 1}, 72 | }, 73 | severity = map_severity(ins.severity), 74 | source = 'Nelua LSP', 75 | message = ins.message, 76 | }) 77 | end 78 | elseif not skip then 79 | server.error(tostring(err)) 80 | end 81 | end 82 | if not skip then 83 | server.send_notification('textDocument/publishDiagnostics', { 84 | uri = uri, 85 | diagnostics = diagnostics, 86 | }) 87 | end 88 | return ast 89 | end 90 | 91 | local function fetch_document(uri, content, skip) 92 | local filepath = utils.uri2path(uri) 93 | local content = content or fs.readfile(filepath) 94 | ast = analyze_ast(content, filepath, uri, skip) 95 | if not skip then 96 | codecache[uri] = content 97 | if ast then 98 | astcache[uri] = ast 99 | end 100 | end 101 | return ast 102 | end 103 | 104 | local function analyze_and_find_loc(uri, textpos, content) 105 | local ast = content and fetch_document(uri, content, true) or astcache[uri] or fetch_document(uri) 106 | if not ast then return end 107 | local content = content or codecache[uri] 108 | local pos = type(textpos) == 'number' and textpos or utils.linecol2pos(content, textpos.line, textpos.character) 109 | if not ast then return end 110 | local nodes = utils.find_nodes_by_pos(ast, pos) 111 | local lastnode = nodes[#nodes] 112 | if not lastnode then return end 113 | local loc = {node=lastnode, nodes=nodes} 114 | if lastnode.attr._symbol then 115 | loc.symbol = lastnode.attr 116 | end 117 | for i=#nodes,1,-1 do -- find scope 118 | local node = nodes[i] 119 | -- utils.dump_table(nodes[i]) 120 | if node.scope then 121 | loc.scope = node.scope 122 | break 123 | end 124 | end 125 | return loc 126 | end 127 | 128 | local function dump_type_info(type, ss, opts) 129 | opts = opts or {} 130 | if not opts.no_header then 131 | ss:addmany('**type** `', type.nickname or type.name, '`\n') 132 | end 133 | ss:addmany('```nelua\n', type:typedesc(),'\n```') 134 | end 135 | 136 | local function node_info(node, attr, opts) 137 | local opts = opts or {} 138 | local ss = sstream() 139 | local attr = attr or node.attr 140 | local type = attr.type 141 | 142 | if type then 143 | local typename = type.name 144 | if type.is_type then 145 | type = attr.value 146 | dump_type_info(type, ss, opts) 147 | elseif type.is_function or type.is_polyfunction then 148 | if attr.value then 149 | type = attr.value 150 | if not opts.no_header then 151 | ss:addmany('**', typename, '** `', type.nickname or type.name, '`\n') 152 | end 153 | ss:add('```nelua\n') 154 | if type.type then 155 | ss:addmany(type.type,'\n') 156 | else 157 | ss:addmany(type.symbol,'\n') 158 | end 159 | ss:add('```') 160 | else 161 | if not opts.no_header then 162 | ss:addmany('**function** `', attr.name, '`\n') 163 | end 164 | if attr.builtin then 165 | ss:add('* builtin function\n') 166 | end 167 | end 168 | elseif type.is_pointer then 169 | ss:add('**pointer**\n') 170 | dump_type_info(type.subtype, ss) 171 | elseif attr.ismethod then 172 | return node_info(nil, attr.calleesym, opts) 173 | else 174 | ss:addmany('**value** `', type, '`\n') 175 | if type.symbol and type.symbol.node and type.symbol.node.attr then 176 | ss:addmany('\n', node_info(nil, type.symbol.node.attr, {no_header = true})) 177 | end 178 | end 179 | end 180 | return ss:tostring() 181 | end 182 | 183 | -- Get hover information 184 | local function hover_method(reqid, params) 185 | local loc = analyze_and_find_loc(params.textDocument.uri, params.position) 186 | if loc then 187 | local value = node_info(loc.node) 188 | server.send_response(reqid, {contents = {kind = 'markdown', value = value}}) 189 | else 190 | server.send_response(reqid, {contents = ''}) 191 | end 192 | end 193 | 194 | local function get_node_ranges(root, tnode, pnode) 195 | local trange = utils.node2textrange(tnode) 196 | local prange = trange 197 | if pnode then 198 | prange = utils.node2textrange(pnode) 199 | else 200 | local pnodes = utils.find_parent_nodes(root, tnode) 201 | if #pnodes then 202 | for _, pnode in ipairs(pnodes) do 203 | if pnode.pos ~= nil then 204 | prange = utils.node2textrange(pnode) 205 | break 206 | end 207 | end 208 | end 209 | end 210 | local uri = utils.path2uri(tnode.src.name) 211 | return { 212 | uri = uri, 213 | range = prange, 214 | selectionRange = trange, 215 | } 216 | end 217 | 218 | local function get_definitioin_symbol(root, snode, tnode) 219 | if not tnode then return nil end 220 | local srange = utils.node2textrange(snode) 221 | local tranges = get_node_ranges(root, tnode) 222 | return { 223 | originSelectionRange = srange, 224 | targetUri = tranges.uri, 225 | targetRange = tranges.range, 226 | targetSelectionRange = tranges.selectionRange, 227 | } 228 | end 229 | 230 | -- Get hover information 231 | local function definition_method(reqid, params) 232 | local loc = analyze_and_find_loc(params.textDocument.uri, params.position) 233 | if not loc then 234 | server.send_response(reqid, {}) 235 | return 236 | end 237 | local rootnode = loc.nodes[1] 238 | local node = loc.node 239 | local list = {} 240 | if node.is_Id then 241 | table.insert(list, get_definitioin_symbol(rootnode, node, loc.symbol.node)) 242 | elseif node.is_call then 243 | table.insert(list, get_definitioin_symbol(rootnode, node, node.attr.calleesym.node)) 244 | elseif node.is_DotIndex then 245 | local sym = loc.symbol or node[2].attr 246 | if sym then 247 | table.insert(list, get_definitioin_symbol(rootnode, node, sym.node)) 248 | end 249 | else 250 | print(node.tag) 251 | end 252 | server.send_response(reqid, list) 253 | end 254 | 255 | local function dump_scope_symbols(ast) 256 | if not ast.scope then return {} end 257 | local list = {} 258 | for _, child in ipairs(ast.scope.children) do 259 | local node = child.node 260 | local item = nil 261 | local xnode = node 262 | if node.is_FuncDef then 263 | local ranges = get_node_ranges(ast, node[2], node) 264 | xnode = node[6] 265 | item = { 266 | name = node[2].attr.name, 267 | detail = tostring(node.attr.type), 268 | kind = 12, 269 | range = ranges.range, 270 | selectionRange = ranges.selectionRange, 271 | } 272 | end 273 | if item then 274 | item.children = dump_scope_symbols(xnode) 275 | table.insert(list, item) 276 | end 277 | end 278 | for name, symbol in spairs(ast.scope.symbols) do 279 | if not symbol.type or symbol.type.is_function or symbol.type.is_polyfunction then 280 | goto continue 281 | end 282 | local node = symbol.node 283 | local ranges = get_node_ranges(ast, node) 284 | local children = {} 285 | local kind = 13 -- variable 286 | local detail = tostring(symbol.type) 287 | if node.is_IdDecl then 288 | local value = node.attr.value 289 | if value and value.node then 290 | local vnode = value.node 291 | if vnode.is_RecordType then 292 | kind = 23 293 | detail = "record" 294 | for _, field in ipairs(vnode) do 295 | local range = utils.node2textrange(field) 296 | table.insert(children, { 297 | name = tostring(field[1]), 298 | detail = tostring(field[2].attr.name), 299 | kind = 8, 300 | range = range, 301 | selectionRange = range, 302 | }) 303 | end 304 | elseif vnode.is_EnumType then 305 | kind = 10 306 | detail = "enum" 307 | if vnode[1] then 308 | detail = string.format("enum(%s)", vnode[1].attr.name) 309 | end 310 | for _, field in ipairs(vnode[2]) do 311 | local range = utils.node2textrange(field) 312 | table.insert(children, { 313 | name = tostring(field[1]), 314 | detail = tostring(field[2] and field[2].attr and field[2].attr.value or ''), 315 | kind = 8, 316 | range = range, 317 | selectionRange = range, 318 | }) 319 | end 320 | end 321 | end 322 | end 323 | table.insert(list, { 324 | name = name, 325 | detail = detail, 326 | kind = kind, 327 | range = ranges.range, 328 | selectionRange = ranges.selectionRange, 329 | children = children, 330 | }) 331 | ::continue:: 332 | end 333 | return list 334 | end 335 | 336 | local function document_symbol(reqid, params) 337 | local ast = fetch_document(params.textDocument.uri) 338 | local list = ast and dump_scope_symbols(ast) or {} 339 | server.send_response(reqid, list) 340 | end 341 | 342 | local function sync_open(reqid, params) 343 | local doc = params.textDocument 344 | if not fetch_document(doc.uri, doc.text) then 345 | server.error('Failed to load document') 346 | end 347 | end 348 | 349 | local function sync_change(reqid, params) 350 | local doc = params.textDocument 351 | local content = params.contentChanges[1].text 352 | fetch_document(doc.uri, content) 353 | end 354 | 355 | local function sync_close(reqid, params) 356 | local doc = params.textDocument 357 | astcache[doc.uri] = nil 358 | codecache[doc.uri] = nil 359 | end 360 | 361 | local function gen_completion_list(scope, out) 362 | if not scope then return end 363 | gen_completion_list(scope.parent, out) 364 | for _, v in ipairs(scope.symbols) do 365 | out[v.name] = tostring(v.type) 366 | end 367 | end 368 | 369 | local function code_completion(reqid, params) 370 | local uri = params.textDocument.uri 371 | local content = codecache[uri] 372 | local textpos = params.position 373 | local pos = utils.linecol2pos(content..'\n', textpos.line, textpos.character) 374 | -- some hack for get ast node 375 | local before = content:sub(1, pos-1):gsub('%a%w*$', '') 376 | local after = content:sub(pos):gsub('^[.:]?%a%w*', '') 377 | local kind = "normal" 378 | -- fake function call 379 | if before:match('[.]$') then 380 | before = before:sub(1, -2)..'()' 381 | kind = "field" 382 | elseif before:match(':$') then 383 | before = before:sub(1, -2)..'()' 384 | kind = "meta" 385 | end 386 | content = before..after 387 | 388 | local ast = analyze_and_find_loc(params.textDocument.uri, #before, content) 389 | local list = {} 390 | if ast then 391 | local node = ast.node 392 | if kind == "field" or kind == "meta" then 393 | if node.is_call then 394 | local attr = node.attr 395 | local xtype = attr.type 396 | local is_instance = false 397 | if not xtype then 398 | attr = node[2].attr 399 | xtype = attr.type 400 | is_instance = true 401 | end 402 | if xtype then 403 | if is_instance then 404 | for k, v in pairs(xtype.metafields) do 405 | if kind == "meta" and v.metafuncselftype ~= nil then 406 | table.insert(list, {label = k, detail = tostring(v)}) 407 | end 408 | end 409 | if kind == "field" then 410 | for k, v in spairs(xtype.fields) do 411 | table.insert(list, {label = k, detail = tostring(v.type)}) 412 | end 413 | end 414 | elseif kind == "field" then 415 | local tab = xtype.is_record and xtype.metafields or xtype.fields 416 | for k, v in spairs(tab) do 417 | if xtype.is_enum then 418 | table.insert(list, {label = k, detail = tostring(xtype)}) 419 | else 420 | table.insert(list, {label = k, detail = tostring(v.type and v.type or v)}) 421 | end 422 | end 423 | end 424 | end 425 | end 426 | elseif kind == "normal" then 427 | local symcache = {} 428 | gen_completion_list(ast.scope, symcache) 429 | for k, v in pairs(symcache) do 430 | table.insert(list, {label = k, detail = v}) 431 | end 432 | end 433 | end 434 | server.send_response(reqid, list) 435 | end 436 | 437 | -- All capabilities supported by this language server. 438 | server.capabilities = { 439 | textDocumentSync = { 440 | openClose = true, 441 | change = 1, 442 | }, 443 | hoverProvider = true, 444 | publishDiagnostics = true, 445 | completionProvider = { 446 | triggerCharacters = { ".", ":" }, 447 | }, 448 | definitionProvider = true, 449 | documentSymbolProvider = true, 450 | } 451 | server.methods = { 452 | ['textDocument/hover'] = hover_method, 453 | ['textDocument/definition'] = definition_method, 454 | ['textDocument/documentSymbol'] = document_symbol, 455 | ['textDocument/didOpen'] = sync_open, 456 | ['textDocument/didChange'] = sync_change, 457 | ['textDocument/didClose'] = sync_close, 458 | ['textDocument/completion'] = code_completion, 459 | } 460 | 461 | -- Listen for requests. 462 | server.listen(io.stdin, stdout) 463 | -------------------------------------------------------------------------------- /nelua-lsp/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | -------------------------------------------------------------------------------- /nelua-lsp/parseerror.lua: -------------------------------------------------------------------------------- 1 | local re = require 'nelua.thirdparty.lpegrex' 2 | 3 | local patt = re.compile[[ 4 | line <== path ":" column ":" row ": " title message 5 | num <-- {[%d]+} -> tonumber 6 | column <-- num 7 | row <-- num 8 | path <-- {([^:] / (":\"))+} 9 | title <-- {[^:]+} ": " 10 | message <-- {.+} 11 | ]] 12 | 13 | return function(content) 14 | local stru = {} 15 | local curr 16 | 17 | for line in content:gmatch("([^\n\r]*)\r?\n?") do 18 | if line == "stack traceback:" then break end 19 | local res = patt:match(line) 20 | if res then 21 | if curr then 22 | table.insert(stru, curr) 23 | end 24 | curr = { 25 | path = res[1], 26 | line = res[2], 27 | character = res[3], 28 | severity = res[4], 29 | message = res[5], 30 | length = 1, 31 | } 32 | elseif curr then 33 | local matched = line:match("^ *([~^]+) *$") 34 | if matched then 35 | curr.message = curr.message:match('(.+)\n') 36 | curr.length = #matched 37 | else 38 | curr.message = curr.message..'\n'..line 39 | end 40 | end 41 | end 42 | if curr then table.insert(stru, curr) end 43 | return stru 44 | end -------------------------------------------------------------------------------- /nelua-lsp/server.lua: -------------------------------------------------------------------------------- 1 | local lfs = require 'lfs' 2 | local console = require 'nelua.utils.console' 3 | local inspect = require 'nelua.thirdparty.inspect' 4 | local json = require 'nelua-lsp.json' 5 | local utils = require 'nelua-lsp.utils' 6 | 7 | local lenfmt = 'Content-Length: %d\r\n\r\n' 8 | 9 | local server = { 10 | -- List of callbacks for each method. 11 | methods = {}, 12 | -- Table of capabilities supported by this language server. 13 | capabilities = {}, 14 | -- Has root path 15 | root_path = nil, 16 | } 17 | 18 | -- Some LSP constants 19 | local LSPErrorsCodes = { 20 | ParseError = -32700, 21 | InvalidRequest = -32600, 22 | MethodNotFound = -32601, 23 | InvalidParams = -32602, 24 | InternalError = -32603, 25 | serverErrorStart = -32099, 26 | serverErrorEnd = -32000, 27 | ServerNotInitialized = -32002, 28 | UnknownErrorCode = -32001, 29 | } 30 | 31 | -- Send a JSON response. 32 | function server.send_response(id, result, error) 33 | local ans = {jsonrpc="2.0", id=id, result=result, error=error} 34 | local content = json.encode(ans) 35 | local header = string.format(lenfmt, #content) 36 | server.stdout:write(header) 37 | server.stdout:write(content) 38 | server.stdout:flush() 39 | end 40 | 41 | -- Send an error response with optional message. 42 | function server.send_error(id, code, message) 43 | if type(code) == 'string' then 44 | -- convert a named code to its numeric error code 45 | message = message or code 46 | code = LSPErrorsCodes[code] 47 | end 48 | message = message or 'Error: '..tostring(code) 49 | server.send_response(id, nil, {code=code, message=message}) 50 | end 51 | 52 | -- Send an notification 53 | function server.send_notification(method, params) 54 | local ans = {jsonrpc="2.0", method=method, params=params} 55 | local content = json.encode(ans) 56 | local header = string.format(lenfmt, #content) 57 | server.stdout:write(header) 58 | server.stdout:write(content) 59 | server.stdout:flush() 60 | end 61 | 62 | -- Show message 63 | function server.error(message) 64 | server.send_notification("window/showMessage", {type=1, message=message}) 65 | end 66 | function server.warn(message) 67 | server.send_notification("window/showMessage", {type=2, message=message}) 68 | end 69 | function server.info(message) 70 | server.send_notification("window/showMessage", {type=3, message=message}) 71 | end 72 | function server.log(message) 73 | server.send_notification("window/showMessage", {type=4, message=message}) 74 | end 75 | 76 | -- Wait and read next JSON request, returning it as a table. 77 | local function read_request() 78 | local header = {} 79 | -- parse all lines from header 80 | while true do 81 | local line = server.stdin:read('L') 82 | line = line:gsub('[\r\n]+$', '') -- strip \r\n from line ending 83 | if line == '' then break end -- empty line means end of header 84 | local field, value = line:match('^([%w-]+):%s*(.*)') 85 | if field and value then 86 | header[field:lower()] = value 87 | end 88 | end 89 | -- check content length 90 | local length = tonumber(header['content-length']) 91 | assert(length and length > 0, 'invalid header content-length') 92 | -- read the content 93 | local content = server.stdin:read(length) 94 | -- parse JSON 95 | return json.decode(content) 96 | end 97 | 98 | -- Listen for incoming requests until the server is requested to shutdown. 99 | function server.listen(stdin, stdout) 100 | server.stdin, server.stdout = stdin, stdout 101 | console.debug('LSP - listening') 102 | local shutdown = false 103 | local initialized = false 104 | for req in read_request do 105 | console.debug('LSP - '..req.method) 106 | if req.method == 'initialize' then 107 | -- send back the supported capabilities 108 | if req.params.rootPath then 109 | server.root_path = req.params.rootPath 110 | elseif req.params.rootUri then 111 | server.root_path = utils.uri2path(req.params.rootUri) 112 | end 113 | if server.root_path then 114 | lfs.chdir(server.root_path) 115 | end 116 | server.send_response(req.id, {capabilities=server.capabilities, serverInfo={name="Nelua LSP Server"}}) 117 | elseif req.method == 'initialized' then 118 | -- both client and server agree on initialization 119 | initialized = true 120 | elseif req.method == 'shutdown' then 121 | -- we now expect an exit method for the next request 122 | shutdown = true 123 | elseif req.method == 'exit' then 124 | -- exit with 0 (success) when shutdown was requested 125 | os.exit(shutdown and 0 or 1) 126 | elseif initialized and not shutdown then 127 | -- process usual requests 128 | local method = server.methods[req.method] 129 | if method then 130 | local ok, err = pcall(method, req.id, req.params) 131 | if not ok then 132 | local errmsg = 'error while handling method:\n'..tostring(err) 133 | server.send_error(req.id, 'InternalError', errmsg) 134 | end 135 | else 136 | console.debug('error: unsupported method "'.. tostring(method)..'"') 137 | -- we must response that we were unable to fulfill the request 138 | server.send_error(req.id, 'MethodNotFound') 139 | end 140 | else -- invalid request when shutting down or initializing 141 | console.debug('error: invalid request "'..tostring(req.method)..'"') 142 | server.send_error(req.id, 'InvalidRequest') 143 | end 144 | end 145 | console.debug('LSP - connection closed') 146 | end 147 | 148 | return server 149 | -------------------------------------------------------------------------------- /nelua-lsp/utils.lua: -------------------------------------------------------------------------------- 1 | local fs = require 'nelua.utils.fs' 2 | local lpegrex = require 'nelua.thirdparty.lpegrex' 3 | local console = require 'nelua.utils.console' 4 | 5 | local utils = {} 6 | 7 | utils.dirsep, utils.pathsep = package.config:match('(.)[\r\n]+(.)[\r\n]+') 8 | utils.is_windows = utils.dirsep == '\\' 9 | 10 | function decodeURI(s) 11 | return string.gsub(s, '%%(%x%x)', function(h) return string.char(tonumber(h, 16)) end) 12 | end 13 | 14 | function encodeURI(s) 15 | s = string.gsub(s, "([^%w%.%- ])", function(c) return string.format("%%%02X", string.byte(c)) end) 16 | return string.gsub(s, " ", "+") 17 | end 18 | 19 | -- Convert a LSP uri to an usable system path. 20 | function utils.uri2path(uri) 21 | local file = uri:match('file://(.*)') 22 | file = decodeURI(file) 23 | if utils.is_windows then 24 | file = string.sub(file:gsub('/', '\\'), 2) 25 | end 26 | file = fs.normpath(file) 27 | return file 28 | end 29 | 30 | function utils.path2uri(path) 31 | if utils.is_windows then 32 | path = '/'..path:gsub('\\', '/') 33 | end 34 | return 'file://'..path 35 | end 36 | 37 | -- Get content position from a line number and column number. 38 | -- The line and column numbers must be zero based (starts at 0). 39 | -- The returned position is one based (starts at 1). 40 | function utils.linecol2pos(content, lineno, colno) 41 | local i = 0 42 | local pos = 0 43 | for line in content:gmatch('[^\r\n]*[\r]?[\n]') do 44 | if i == lineno then 45 | pos = pos + colno 46 | break 47 | end 48 | i = i + 1 49 | pos = pos + #line 50 | end 51 | return pos + 1 -- convert to one-based 52 | end 53 | 54 | local function textpos2string(pos) 55 | return string.format('%d:%d', pos.line+1, pos.character+1) 56 | end 57 | 58 | local function textpos(lineno, colno) 59 | if colno <= 1 then colno = 1 end 60 | return setmetatable({line=lineno-1, character=colno-1}, {__tostring=textpos2string}) 61 | end 62 | 63 | -- Convert content position into a table to send back in LSP API. 64 | function utils.pos2textpos(content, pos) 65 | local lineno, colno = lpegrex.calcline(content, pos) 66 | return textpos(lineno, colno) -- convert to zero-based 67 | end 68 | 69 | local function textrange2string(range) 70 | return string.format('[%s-%s]', range['start'], range['end']) 71 | end 72 | 73 | local function textrange(a, b) 74 | return setmetatable({["start"]=a, ["end"]=b}, {__tostring=textrange2string}) 75 | end 76 | 77 | -- Convert a content position range to a table to send back in LSP API. 78 | function utils.posrange2textrange(content, startpos, endpos) 79 | return textrange( 80 | utils.pos2textpos(content, startpos), 81 | utils.pos2textpos(content, endpos) 82 | ) 83 | end 84 | 85 | function utils.node2textrange(node) 86 | return utils.posrange2textrange(node.src.content, node.pos, node.endpos) 87 | end 88 | 89 | local function find_parent_nodes(node, target, foundnodes) 90 | if type(node) ~= 'table' then return end 91 | for i=1,node.nargs or #node do 92 | local curr = node[i] 93 | if curr == target then 94 | table.insert(foundnodes, node) 95 | return true 96 | end 97 | if find_parent_nodes(curr, target, foundnodes) then 98 | table.insert(foundnodes, node) 99 | return true 100 | end 101 | end 102 | end 103 | 104 | local function find_nodes_by_pos(node, pos, foundnodes) 105 | if type(node) ~= 'table' then return end 106 | if node._astnode and 107 | node.pos and pos >= node.pos and 108 | node.endpos and pos < node.endpos then 109 | foundnodes[#foundnodes+1] = node 110 | end 111 | for i=1,node.nargs or #node do 112 | find_nodes_by_pos(node[i], pos, foundnodes) 113 | end 114 | end 115 | 116 | -- Find node's parent chain 117 | function utils.find_parent_nodes(node, target) 118 | local foundnodes = {} 119 | find_parent_nodes(node, target, foundnodes) 120 | return foundnodes 121 | end 122 | 123 | -- Find all nodes that contains the position. 124 | function utils.find_nodes_by_pos(node, pos) 125 | local foundnodes = {} 126 | find_nodes_by_pos(node, pos, foundnodes) 127 | return foundnodes 128 | end 129 | 130 | function utils.dump_table(table, opts) 131 | opts = opts or {} 132 | if not table then 133 | console.debug '(nil)' 134 | return 135 | end 136 | if type(table) ~= 'table' then 137 | console.debug('('..type(table)..')') 138 | return 139 | end 140 | if opts.meta then 141 | for k, v in pairs(getmetatable(table)) do 142 | console.debugf("#%s = %s", k, tostring(v)) 143 | end 144 | end 145 | for k, v in pairs(table) do 146 | console.debugf("%s = %s", k, tostring(v)) 147 | end 148 | end 149 | 150 | return utils 151 | --------------------------------------------------------------------------------