diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..b5e37a5 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,8 @@ +{ + "workspace.library": [ + "./lsp" + ], + "diagnostics.disable": [ + "undefined-global" + ] +} diff --git a/lsp/dkjson.lua b/lsp/dkjson.lua deleted file mode 100644 index 73b3695..0000000 --- a/lsp/dkjson.lua +++ /dev/null @@ -1,816 +0,0 @@ --- Module options: -local always_use_lpeg = false -local register_global_module_table = false -local global_module_name = "json" - ---[==[ - -David Kolf's JSON module for Lua 5.1 - 5.4 - -Version 2.8 - - -For the documentation see the corresponding readme.txt or visit -. - -You can contact the author by sending an e-mail to 'david' at the -domain 'dkolf.de'. - - -Copyright (C) 2010-2024 David Heiko Kolf - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---]==] - --- global dependencies: -local pairs, type, tostring, tonumber, getmetatable, setmetatable = - pairs, type, tostring, tonumber, getmetatable, setmetatable -local error, require, pcall, select = error, require, pcall, select -local floor, huge = math.floor, math.huge -local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = - string.rep, string.gsub, string.sub, string.byte, string.char, string.find, string.len, string.format -local strmatch = string.match -local concat = table.concat - -local json = { version = "dkjson 2.8" } - -local jsonlpeg = {} - -if register_global_module_table then - if always_use_lpeg then - _G[global_module_name] = jsonlpeg - else - _G[global_module_name] = json - end -end - -local _ENV = nil -- blocking globals in Lua 5.2 and later - -pcall(function() - -- Enable access to blocked metatables. - -- Don't worry, this module doesn't change anything in them. - local debmeta = require("debug").getmetatable - if debmeta then - getmetatable = debmeta - end -end) - -json.null = setmetatable({}, { - __tojson = function() - return "null" - end, -}) - -local function isarray(tbl) - local max, n, arraylen = 0, 0, 0 - for k, v in pairs(tbl) do - if k == "n" and type(v) == "number" then - arraylen = v - if v > max then - max = v - end - else - if type(k) ~= "number" or k < 1 or floor(k) ~= k then - return false - end - if k > max then - max = k - end - n = n + 1 - end - end - if max > 10 and max > arraylen and max > n * 2 then - return false -- don't create an array with too many holes - end - return true, max -end - -local escapecodes = { - ['"'] = '\\"', - ["\\"] = "\\\\", - ["\b"] = "\\b", - ["\f"] = "\\f", - ["\n"] = "\\n", - ["\r"] = "\\r", - ["\t"] = "\\t", -} - -local function escapeutf8(uchar) - local value = escapecodes[uchar] - if value then - return value - end - local a, b, c, d = strbyte(uchar, 1, 4) - a, b, c, d = a or 0, b or 0, c or 0, d or 0 - if a <= 0x7f then - value = a - elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then - value = (a - 0xc0) * 0x40 + b - 0x80 - elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then - value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 - elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then - value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 - else - return "" - end - if value <= 0xffff then - return strformat("\\u%.4x", value) - elseif value <= 0x10ffff then - -- encode as UTF-16 surrogate pair - value = value - 0x10000 - local highsur, lowsur = 0xD800 + floor(value / 0x400), 0xDC00 + (value % 0x400) - return strformat("\\u%.4x\\u%.4x", highsur, lowsur) - else - return "" - end -end - -local function fsub(str, pattern, repl) - -- gsub always builds a new string in a buffer, even when no match - -- exists. First using find should be more efficient when most strings - -- don't contain the pattern. - if strfind(str, pattern) then - return gsub(str, pattern, repl) - else - return str - end -end - -local function quotestring(value) - -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js - value = fsub(value, '[%z\1-\31"\\\127]', escapeutf8) - if strfind(value, "[\194\216\220\225\226\239]") then - value = fsub(value, "\194[\128-\159\173]", escapeutf8) - value = fsub(value, "\216[\128-\132]", escapeutf8) - value = fsub(value, "\220\143", escapeutf8) - value = fsub(value, "\225\158[\180\181]", escapeutf8) - value = fsub(value, "\226\128[\140-\143\168-\175]", escapeutf8) - value = fsub(value, "\226\129[\160-\175]", escapeutf8) - value = fsub(value, "\239\187\191", escapeutf8) - value = fsub(value, "\239\191[\176-\191]", escapeutf8) - end - return '"' .. value .. '"' -end -json.quotestring = quotestring - -local function replace(str, o, n) - local i, j = strfind(str, o, 1, true) - if i then - return strsub(str, 1, i - 1) .. n .. strsub(str, j + 1, -1) - else - return str - end -end - --- locale independent num2str and str2num functions -local decpoint, numfilter - -local function updatedecpoint() - decpoint = strmatch(tostring(0.5), "([^05+])") - -- build a filter that can be used to remove group separators - numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" -end - -updatedecpoint() - -local function num2str(num) - return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") -end - -local function str2num(str) - local num = tonumber(replace(str, ".", decpoint)) - if not num then - updatedecpoint() - num = tonumber(replace(str, ".", decpoint)) - end - return num -end - -local function addnewline2(level, buffer, buflen) - buffer[buflen + 1] = "\n" - buffer[buflen + 2] = strrep(" ", level) - buflen = buflen + 2 - return buflen -end - -function json.addnewline(state) - if state.indent then - state.bufferlen = addnewline2(state.level or 0, state.buffer, state.bufferlen or #state.buffer) - end -end - -local encode2 -- forward declaration - -local function addpair(key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) - local kt = type(key) - if kt ~= "string" and kt ~= "number" then - return nil, "type '" .. kt .. "' is not supported as a key by JSON." - end - if prev then - buflen = buflen + 1 - buffer[buflen] = "," - end - if indent then - buflen = addnewline2(level, buffer, buflen) - end - -- When Lua is compiled with LUA_NOCVTN2S this will fail when - -- numbers are mixed into the keys of the table. JSON keys are always - -- strings, so this would be an implicit conversion too and the failure - -- is intentional. - buffer[buflen + 1] = quotestring(key) - buffer[buflen + 2] = ":" - return encode2(value, indent, level, buffer, buflen + 2, tables, globalorder, state) -end - -local function appendcustom(res, buffer, state) - local buflen = state.bufferlen - if type(res) == "string" then - buflen = buflen + 1 - buffer[buflen] = res - end - return buflen -end - -local function exception(reason, value, state, buffer, buflen, defaultmessage) - defaultmessage = defaultmessage or reason - local handler = state.exception - if not handler then - return nil, defaultmessage - else - state.bufferlen = buflen - local ret, msg = handler(reason, value, state, defaultmessage) - if not ret then - return nil, msg or defaultmessage - end - return appendcustom(ret, buffer, state) - end -end - -function json.encodeexception(reason, value, state, defaultmessage) - return quotestring("<" .. defaultmessage .. ">") -end - -encode2 = function(value, indent, level, buffer, buflen, tables, globalorder, state) - local valtype = type(value) - local valmeta = getmetatable(value) - valmeta = type(valmeta) == "table" and valmeta -- only tables - local valtojson = valmeta and valmeta.__tojson - if valtojson then - if tables[value] then - return exception("reference cycle", value, state, buffer, buflen) - end - tables[value] = true - state.bufferlen = buflen - local ret, msg = valtojson(value, state) - if not ret then - return exception("custom encoder failed", value, state, buffer, buflen, msg) - end - tables[value] = nil - buflen = appendcustom(ret, buffer, state) - elseif value == nil then - buflen = buflen + 1 - buffer[buflen] = "null" - elseif valtype == "number" then - local s - if value ~= value or value >= huge or -value >= huge then - -- This is the behaviour of the original JSON implementation. - s = "null" - else - s = num2str(value) - end - buflen = buflen + 1 - buffer[buflen] = s - elseif valtype == "boolean" then - buflen = buflen + 1 - buffer[buflen] = value and "true" or "false" - elseif valtype == "string" then - buflen = buflen + 1 - buffer[buflen] = quotestring(value) - elseif valtype == "table" then - if tables[value] then - return exception("reference cycle", value, state, buffer, buflen) - end - tables[value] = true - level = level + 1 - local isa, n = isarray(value) - if n == 0 and valmeta and valmeta.__jsontype == "object" then - isa = false - end - local msg - if isa then -- JSON array - buflen = buflen + 1 - buffer[buflen] = "[" - for i = 1, n do - buflen, msg = encode2(value[i], indent, level, buffer, buflen, tables, globalorder, state) - if not buflen then - return nil, msg - end - if i < n then - buflen = buflen + 1 - buffer[buflen] = "," - end - end - buflen = buflen + 1 - buffer[buflen] = "]" - else -- JSON object - local prev = false - buflen = buflen + 1 - buffer[buflen] = "{" - local order = valmeta and valmeta.__jsonorder or globalorder - if order then - local used = {} - n = #order - for i = 1, n do - local k = order[i] - local v = value[k] - if v ~= nil then - used[k] = true - buflen, msg = addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) - if not buflen then - return nil, msg - end - prev = true -- add a seperator before the next element - end - end - for k, v in pairs(value) do - if not used[k] then - buflen, msg = addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) - if not buflen then - return nil, msg - end - prev = true -- add a seperator before the next element - end - end - else -- unordered - for k, v in pairs(value) do - buflen, msg = addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) - if not buflen then - return nil, msg - end - prev = true -- add a seperator before the next element - end - end - if indent then - buflen = addnewline2(level - 1, buffer, buflen) - end - buflen = buflen + 1 - buffer[buflen] = "}" - end - tables[value] = nil - else - return exception( - "unsupported type", - value, - state, - buffer, - buflen, - "type '" .. valtype .. "' is not supported by JSON." - ) - end - return buflen -end - -function json.encode(value, state) - state = state or {} - local oldbuffer = state.buffer - local buffer = oldbuffer or {} - state.buffer = buffer - updatedecpoint() - local ret, msg = encode2( - value, - state.indent, - state.level or 0, - buffer, - state.bufferlen or 0, - state.tables or {}, - state.keyorder, - state - ) - if not ret then - error(msg, 2) - elseif oldbuffer == buffer then - state.bufferlen = ret - return true - else - state.bufferlen = nil - state.buffer = nil - return concat(buffer) - end -end - -local function loc(str, where) - local line, pos, linepos = 1, 1, 0 - while true do - pos = strfind(str, "\n", pos, true) - if pos and pos < where then - line = line + 1 - linepos = pos - pos = pos + 1 - else - break - end - end - return strformat("line %d, column %d", line, where - linepos) -end - -local function unterminated(str, what, where) - return nil, strlen(str) + 1, "unterminated " .. what .. " at " .. loc(str, where) -end - -local function scanwhite(str, pos) - while true do - pos = strfind(str, "%S", pos) - if not pos then - return nil - end - local sub2 = strsub(str, pos, pos + 1) - if sub2 == "\239\187" and strsub(str, pos + 2, pos + 2) == "\191" then - -- UTF-8 Byte Order Mark - pos = pos + 3 - elseif sub2 == "//" then - pos = strfind(str, "[\n\r]", pos + 2) - if not pos then - return nil - end - elseif sub2 == "/*" then - pos = strfind(str, "*/", pos + 2) - if not pos then - return nil - end - pos = pos + 2 - else - return pos - end - end -end - -local escapechars = { - ['"'] = '"', - ["\\"] = "\\", - ["/"] = "/", - ["b"] = "\b", - ["f"] = "\f", - ["n"] = "\n", - ["r"] = "\r", - ["t"] = "\t", -} - -local function unichar(value) - if value < 0 then - return nil - elseif value <= 0x007f then - return strchar(value) - elseif value <= 0x07ff then - return strchar(0xc0 + floor(value / 0x40), 0x80 + (floor(value) % 0x40)) - elseif value <= 0xffff then - return strchar(0xe0 + floor(value / 0x1000), 0x80 + (floor(value / 0x40) % 0x40), 0x80 + (floor(value) % 0x40)) - elseif value <= 0x10ffff then - return strchar( - 0xf0 + floor(value / 0x40000), - 0x80 + (floor(value / 0x1000) % 0x40), - 0x80 + (floor(value / 0x40) % 0x40), - 0x80 + (floor(value) % 0x40) - ) - else - return nil - end -end - -local function scanstring(str, pos) - local lastpos = pos + 1 - local buffer, n = {}, 0 - while true do - local nextpos = strfind(str, '["\\]', lastpos) - if not nextpos then - return unterminated(str, "string", pos) - end - if nextpos > lastpos then - n = n + 1 - buffer[n] = strsub(str, lastpos, nextpos - 1) - end - if strsub(str, nextpos, nextpos) == '"' then - lastpos = nextpos + 1 - break - else - local escchar = strsub(str, nextpos + 1, nextpos + 1) - local value - if escchar == "u" then - value = tonumber(strsub(str, nextpos + 2, nextpos + 5), 16) - if value then - local value2 - if 0xD800 <= value and value <= 0xDBff then - -- we have the high surrogate of UTF-16. Check if there is a - -- low surrogate escaped nearby to combine them. - if strsub(str, nextpos + 6, nextpos + 7) == "\\u" then - value2 = tonumber(strsub(str, nextpos + 8, nextpos + 11), 16) - if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then - value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 - else - value2 = nil -- in case it was out of range for a low surrogate - end - end - end - value = value and unichar(value) - if value then - if value2 then - lastpos = nextpos + 12 - else - lastpos = nextpos + 6 - end - end - end - end - if not value then - value = escapechars[escchar] or escchar - lastpos = nextpos + 2 - end - n = n + 1 - buffer[n] = value - end - end - if n == 1 then - return buffer[1], lastpos - elseif n > 1 then - return concat(buffer), lastpos - else - return "", lastpos - end -end - -local scanvalue -- forward declaration - -local function scantable(what, closechar, str, startpos, nullval, objectmeta, arraymeta) - local tbl, n = {}, 0 - local pos = startpos + 1 - if what == "object" then - setmetatable(tbl, objectmeta) - else - setmetatable(tbl, arraymeta) - end - while true do - pos = scanwhite(str, pos) - if not pos then - return unterminated(str, what, startpos) - end - local char = strsub(str, pos, pos) - if char == closechar then - return tbl, pos + 1 - end - local val1, err - val1, pos, err = scanvalue(str, pos, nullval, objectmeta, arraymeta) - if err then - return nil, pos, err - end - pos = scanwhite(str, pos) - if not pos then - return unterminated(str, what, startpos) - end - char = strsub(str, pos, pos) - if char == ":" then - if val1 == nil then - return nil, pos, "cannot use nil as table index (at " .. loc(str, pos) .. ")" - end - pos = scanwhite(str, pos + 1) - if not pos then - return unterminated(str, what, startpos) - end - local val2 - val2, pos, err = scanvalue(str, pos, nullval, objectmeta, arraymeta) - if err then - return nil, pos, err - end - tbl[val1] = val2 - pos = scanwhite(str, pos) - if not pos then - return unterminated(str, what, startpos) - end - char = strsub(str, pos, pos) - else - n = n + 1 - tbl[n] = val1 - end - if char == "," then - pos = pos + 1 - end - end -end - -scanvalue = function(str, pos, nullval, objectmeta, arraymeta) - pos = pos or 1 - pos = scanwhite(str, pos) - if not pos then - return nil, strlen(str) + 1, "no valid JSON value (reached the end)" - end - local char = strsub(str, pos, pos) - if char == "{" then - return scantable("object", "}", str, pos, nullval, objectmeta, arraymeta) - elseif char == "[" then - return scantable("array", "]", str, pos, nullval, objectmeta, arraymeta) - elseif char == '"' then - return scanstring(str, pos) - else - local pstart, pend = strfind(str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) - if pstart then - local number = str2num(strsub(str, pstart, pend)) - if number then - return number, pend + 1 - end - end - pstart, pend = strfind(str, "^%a%w*", pos) - if pstart then - local name = strsub(str, pstart, pend) - if name == "true" then - return true, pend + 1 - elseif name == "false" then - return false, pend + 1 - elseif name == "null" then - return nullval, pend + 1 - end - end - return nil, pos, "no valid JSON value at " .. loc(str, pos) - end -end - -local function optionalmetatables(...) - if select("#", ...) > 0 then - return ... - else - return { __jsontype = "object" }, { __jsontype = "array" } - end -end - -function json.decode(str, pos, nullval, ...) - local objectmeta, arraymeta = optionalmetatables(...) - return scanvalue(str, pos, nullval, objectmeta, arraymeta) -end - -function json.use_lpeg() - local g = require("lpeg") - - if type(g.version) == "function" and g.version() == "0.11" then - error("due to a bug in LPeg 0.11, it cannot be used for JSON matching") - end - - local pegmatch = g.match - local P, S, R = g.P, g.S, g.R - - local function ErrorCall(str, pos, msg, state) - if not state.msg then - state.msg = msg .. " at " .. loc(str, pos) - state.pos = pos - end - return false - end - - local function Err(msg) - return g.Cmt(g.Cc(msg) * g.Carg(2), ErrorCall) - end - - local function ErrorUnterminatedCall(str, pos, what, state) - return ErrorCall(str, pos - 1, "unterminated " .. what, state) - end - - local SingleLineComment = P("//") * (1 - S("\n\r")) ^ 0 - local MultiLineComment = P("/*") * (1 - P("*/")) ^ 0 * P("*/") - local Space = (S(" \n\r\t") + P("\239\187\191") + SingleLineComment + MultiLineComment) ^ 0 - - local function ErrUnterminated(what) - return g.Cmt(g.Cc(what) * g.Carg(2), ErrorUnterminatedCall) - end - - local PlainChar = 1 - S('"\\\n\r') - local EscapeSequence = (P("\\") * g.C(S('"\\/bfnrt') + Err("unsupported escape sequence"))) / escapechars - local HexDigit = R("09", "af", "AF") - local function UTF16Surrogate(match, pos, high, low) - high, low = tonumber(high, 16), tonumber(low, 16) - if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then - return true, unichar((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) - else - return false - end - end - local function UTF16BMP(hex) - return unichar(tonumber(hex, 16)) - end - local U16Sequence = (P("\\u") * g.C(HexDigit * HexDigit * HexDigit * HexDigit)) - local UnicodeEscape = g.Cmt(U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence / UTF16BMP - local Char = UnicodeEscape + EscapeSequence + PlainChar - local String = P('"') * (g.Cs(Char ^ 0) * P('"') + ErrUnterminated("string")) - local Integer = P("-") ^ -1 * (P("0") + (R("19") * R("09") ^ 0)) - local Fractal = P(".") * R("09") ^ 0 - local Exponent = (S("eE")) * (S("+-")) ^ -1 * R("09") ^ 1 - local Number = (Integer * Fractal ^ -1 * Exponent ^ -1) / str2num - local Constant = P("true") * g.Cc(true) + P("false") * g.Cc(false) + P("null") * g.Carg(1) - local SimpleValue = Number + String + Constant - local ArrayContent, ObjectContent - - -- The functions parsearray and parseobject parse only a single value/pair - -- at a time and store them directly to avoid hitting the LPeg limits. - local function parsearray(str, pos, nullval, state) - local obj, cont - local start = pos - local npos - local t, nt = {}, 0 - repeat - obj, cont, npos = pegmatch(ArrayContent, str, pos, nullval, state) - if cont == "end" then - return ErrorUnterminatedCall(str, start, "array", state) - end - pos = npos - if cont == "cont" or cont == "last" then - nt = nt + 1 - t[nt] = obj - end - until cont ~= "cont" - return pos, setmetatable(t, state.arraymeta) - end - - local function parseobject(str, pos, nullval, state) - local obj, key, cont - local start = pos - local npos - local t = {} - repeat - key, obj, cont, npos = pegmatch(ObjectContent, str, pos, nullval, state) - if cont == "end" then - return ErrorUnterminatedCall(str, start, "object", state) - end - pos = npos - if cont == "cont" or cont == "last" then - t[key] = obj - end - until cont ~= "cont" - return pos, setmetatable(t, state.objectmeta) - end - - local Array = P("[") * g.Cmt(g.Carg(1) * g.Carg(2), parsearray) - local Object = P("{") * g.Cmt(g.Carg(1) * g.Carg(2), parseobject) - local Value = Space * (Array + Object + SimpleValue) - local ExpectedValue = Value + Space * Err("value expected") - local ExpectedKey = String + Err("key expected") - local End = P(-1) * g.Cc("end") - local ErrInvalid = Err("invalid JSON") - ArrayContent = ( - Value * Space * (P(",") * g.Cc("cont") + P("]") * g.Cc("last") + End + ErrInvalid) - + g.Cc(nil) * (P("]") * g.Cc("empty") + End + ErrInvalid) - ) * g.Cp() - local Pair = g.Cg(Space * ExpectedKey * Space * (P(":") + Err("colon expected")) * ExpectedValue) - ObjectContent = ( - g.Cc(nil) * g.Cc(nil) * P("}") * g.Cc("empty") - + End - + (Pair * Space * (P(",") * g.Cc("cont") + P("}") * g.Cc("last") + End + ErrInvalid) + ErrInvalid) - ) * g.Cp() - local DecodeValue = ExpectedValue * g.Cp() - - jsonlpeg.version = json.version - jsonlpeg.encode = json.encode - jsonlpeg.null = json.null - jsonlpeg.quotestring = json.quotestring - jsonlpeg.addnewline = json.addnewline - jsonlpeg.encodeexception = json.encodeexception - jsonlpeg.using_lpeg = true - - function jsonlpeg.decode(str, pos, nullval, ...) - local state = {} - state.objectmeta, state.arraymeta = optionalmetatables(...) - local obj, retpos = pegmatch(DecodeValue, str, pos, nullval, state) - if state.msg then - return nil, state.pos, state.msg - else - return obj, retpos - end - end - - -- cache result of this function: - json.use_lpeg = function() - return jsonlpeg - end - jsonlpeg.use_lpeg = json.use_lpeg - - return jsonlpeg -end - -if always_use_lpeg then - return json.use_lpeg() -end - -return json diff --git a/main.go b/main.go index 5d993f9..e523617 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "fes/modules/config" "fes/modules/doc" + "fes/modules/lsp" "fes/modules/new" "fes/modules/server" "fes/modules/version" @@ -19,6 +20,9 @@ import ( //go:embed lib/* var lib embed.FS +//go:embed lsp/* +var lspStubs embed.FS + //go:embed index.html var documentation string @@ -27,20 +31,23 @@ func init() { config.Color = flag.Bool("no-color", false, "Disable color output") config.Static = flag.Bool("static", false, "Render and save all pages") config.Docker = flag.Bool("docker", false, "Create a docker project") + config.Lib = lib + config.LspStubs = lspStubs config.Doc = documentation } func main() { flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] \n", os.Args[0]) - fmt.Fprintf(os.Stderr, "Commands:") - fmt.Fprintf(os.Stderr, " new Create a new project") - fmt.Fprintf(os.Stderr, " doc Open documentation") - fmt.Fprintf(os.Stderr, " run Start the server") - fmt.Fprintf(os.Stderr, "Options:") + fmt.Fprintln(flag.CommandLine.Output(), "Commands:") + fmt.Fprintln(flag.CommandLine.Output(), " new Create a new project") + fmt.Fprintln(flag.CommandLine.Output(), " doc Open documentation") + fmt.Fprintln(flag.CommandLine.Output(), " run Start the server") + fmt.Fprintln(flag.CommandLine.Output(), " lsp Work with Lsp") + fmt.Fprintln(flag.CommandLine.Output(), "Options:") flag.PrintDefaults() - fmt.Fprintf(os.Stderr, "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.") + fmt.Fprintln(flag.CommandLine.Output(), "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.") } showVersion := flag.Bool("version", false, "Show version and exit") @@ -67,32 +74,39 @@ func main() { } cmd := args[0] - var dir string - if cmd == "new" || cmd == "run" { - if len(args) < 2 { - fmt.Fprintf(os.Stderr, "Error: %s requires \n", cmd) - flag.Usage() - os.Exit(1) - } - dir = args[1] - } + var arg string - switch cmd { - case "new": - if err := new.Project(dir); err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) - os.Exit(1) - } - case "doc": + if cmd == "doc" { if err := doc.Open(); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } + os.Exit(0) + } + + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "Error: not enough arguments") + flag.Usage() + os.Exit(1) + } + arg = args[1] + + switch cmd { + case "new": + if err := new.Project(arg); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + case "lsp": + if err := lsp.Do(arg); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } case "run": - if err := server.Start(dir); err != nil { + if err := server.Start(arg); err != nil { if errors.Is(err, os.ErrNotExist) { - fmt.Fprintf(os.Stderr, "%s does not exist\n", dir) - fmt.Fprintf(os.Stderr, "Try: fes new %s\n", dir) + fmt.Fprintf(os.Stderr, "%s does not exist\n", arg) + fmt.Fprintf(os.Stderr, "Try: fes new %s\n", arg) os.Exit(1) } else { fmt.Fprintln(os.Stderr, "Error:", err) diff --git a/modules/config/config.go b/modules/config/config.go index 1a9ba14..9836920 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -6,6 +6,8 @@ import ( ) var Lib embed.FS +var LspStubs embed.FS + var Doc string var Port *int var Color *bool diff --git a/modules/lsp/lsp.go b/modules/lsp/lsp.go new file mode 100644 index 0000000..4b64e78 --- /dev/null +++ b/modules/lsp/lsp.go @@ -0,0 +1,173 @@ +package lsp + +import ( + "embed" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "fes/modules/config" +) + +var destRoot string = filepath.Join(func () string { + res, err := os.UserHomeDir() + if err != nil { panic(err) } + return res +}(), ".local", "LuaAddons", "fes") + +func usage(status int) { + fmt.Fprintln(flag.CommandLine.Output(), "Lsp Usage:") + fmt.Fprintln(flag.CommandLine.Output(), " help Print this help and exit") + fmt.Fprintln(flag.CommandLine.Output(), " install Install the Lsp") + fmt.Fprintln(flag.CommandLine.Output(), " uninstall Uninstall the Lsp") + fmt.Fprintln(flag.CommandLine.Output(), " doctor Verify Lsp installation") + os.Exit(status) +} + +func writeFile(fs embed.FS, src, dst string) error { + in, err := fs.Open(src) + if err != nil { + return err + } + defer in.Close() + + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} + +func install() error { + if err := os.RemoveAll(destRoot); err != nil { + return err + } + + var found bool + + err := fsWalkDir(config.LspStubs, "lsp", func(path string) error { + if path == "lsp" { + return nil + } + + rel := strings.TrimPrefix(path, "lsp/") + dst := filepath.Join("library", destRoot, rel) + + info, err := fsStat(config.LspStubs, path) + if err != nil { + return err + } + + if info.IsDir() { + return os.MkdirAll(dst, 0755) + } + + found = true + return writeFile(config.LspStubs, path, dst) + }) + + if err != nil { + return err + } + + if !found { + return errors.New("no lsp stubs found in embedded fs") + } + + err = os.WriteFile(filepath.Join("dstRoot", "config.json"), []byte(""), 0755) + + fmt.Println("Lua LSP installed to:", destRoot) + return nil +} + +func uninstall() error { + if _, err := os.Stat(destRoot); err != nil { + if os.IsNotExist(err) { + fmt.Println("Lua LSP not installed") + return nil + } + return err + } + + if err := os.RemoveAll(destRoot); err != nil { + return err + } + + fmt.Println("Lua LSP uninstalled from:", destRoot) + return nil +} + +func doctor() error { + info, err := os.Stat(destRoot) + if err != nil { + if os.IsNotExist(err) { + return errors.New("lsp not installed") + } + return err + } + + if !info.IsDir() { + return errors.New("lsp install path is not a directory") + } + + var luaFiles int + + err = filepath.Walk(destRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(info.Name(), ".lua") { + luaFiles++ + } + return nil + }) + if err != nil { + return err + } + + if luaFiles == 0 { + return errors.New("no lua stubs found in lsp directory") + } + + fmt.Println("LSP stubs: OK") + fmt.Println("Location:", destRoot) + fmt.Println() + fmt.Println("LuaLS configuration required:") + fmt.Println() + fmt.Println(` "Lua.workspace.library": {`) + fmt.Printf(` "%s": true`, destRoot) + fmt.Println() + fmt.Println(` }`) + fmt.Println() + fmt.Println("Restart your editor after installing or updating the LSP stubs") + + return nil +} + +func Do(arg string) error { + switch arg { + case "help": + usage(0) + case "install": + return install() + case "uninstall": + return uninstall() + case "doctor": + return doctor() + default: + fmt.Fprintf(os.Stderr, "Unknown command: %s\n", arg) + usage(1) + } + return nil +} diff --git a/modules/lsp/util.go b/modules/lsp/util.go new file mode 100644 index 0000000..b372a8a --- /dev/null +++ b/modules/lsp/util.go @@ -0,0 +1,36 @@ +package lsp + +import ( + "embed" + "os" + "path/filepath" +) + +func fsWalkDir(fs embed.FS, root string, fn func(string) error) error { + entries, err := fs.ReadDir(root) + if err != nil { + return err + } + + for _, e := range entries { + p := filepath.Join(root, e.Name()) + if err := fn(p); err != nil { + return err + } + if e.IsDir() { + if err := fsWalkDir(fs, p, fn); err != nil { + return err + } + } + } + return nil +} + +func fsStat(fs embed.FS, path string) (os.FileInfo, error) { + f, err := fs.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return f.Stat() +}