Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lua/lspsettings/config.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
--- @class Config
Config = {
json5 = true,
paths = {
vim.fs.joinpath(vim.fn.stdpath('config'), "lspsettings"),
-- compatibility with `nlsp-settings` plugin
Expand Down Expand Up @@ -58,4 +59,20 @@ function Config:extend(opts)
end
end

--- Returns supported settings files extensions
--- @param prefix? string Add prefix to all extensions
--- @return string[]
function Config:extensions(prefix)
local extensions = { "json" }

if self.json5 then
extensions = { "json5", "jsonc", "json" }
end

if prefix then
return vim.fn.map(extensions, function(_, ext) return prefix .. ext end)
end
return extensions
end

return Config
19 changes: 16 additions & 3 deletions lua/lspsettings/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,20 @@ M.open_settings_file = function(args)
if not path then return end

vim.fn.mkdir(path, "p")
vim.cmd.edit(vim.fs.joinpath(path, server_name .. ".json"))

local file_names = M.config:extensions(server_name .. ".")
local file_name = file_names[1]

-- Trying to find existing file
for _, fname in ipairs(file_names) do
local file_path = vim.fs.joinpath(path, fname)
if vim.fn.filereadable(file_path) == 1 then
file_name = fname
break
end
end

vim.cmd.edit(vim.fs.joinpath(path, file_name))
end

--- Setup to read from a settings file.
Expand All @@ -86,13 +99,13 @@ M.setup = function(opts)

-- Bind callback to watch changes in configuration files
vim.api.nvim_create_autocmd("BufWritePost", {
pattern = "*.json",
pattern = M.config:extensions("*."),
callback = function(event_data)
local full_path = event_data.match
local relative_path = event_data.file
local fname = vim.fs.basename(relative_path)

local server_name = fname:sub(1, -6)
local server_name = fname:gsub("%.json[c5]?$", "")

-- Check that this `server_name` really exists
if not vim.lsp.config[server_name] then
Expand Down
191 changes: 191 additions & 0 deletions lua/lspsettings/json5.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
---@diagnostic disable: lowercase-global

--- @param str string Stringified JSON5 data
--- @param opts table<string,any> Options similar to `vim.json.decode`. Not used for now
---@diagnostic disable-next-line: unused-local
local function decode(str, opts)
local function tokenize(input)
local tokens = {}
local i = 1
local len = #input

local function skip_whitespace()
while i <= len do
local c = input:sub(i, i)
if c:match("[%s\n\r\t]") then
i = i + 1
elseif c == "/" and input:sub(i + 1, i + 1) == "/" then
i = i + 2
while i <= len and input:sub(i, i) ~= "\n" do
i = i + 1
end
elseif c == "/" and input:sub(i + 1, i + 1) == "*" then
i = i + 2
while i <= len - 1 and not (input:sub(i, i + 1) == "*/") do
i = i + 1
end
i = i + 2
else
break
end
end
end

local function read_line(quote)
i = i + 1
local output = ""
while i <= len do
local c = input:sub(i, i)
if c == "\\" then
local nextc = input:sub(i + 1, i + 1)
if nextc == "n" then
output = output .. "\n"
elseif nextc == "t" then
output = output .. "\t"
elseif nextc == "r" then
output = output .. "\r"
elseif nextc == quote then
output = output .. quote
else
output = output .. nextc
end
i = i + 2
elseif c == quote then
i = i + 1
break
else
output = output .. c
i = i + 1
end
end
return { type = "string", value = output }
end

local function read_number()
local start = i
while i <= len and input:sub(i, i):match("[%+%-%.%deE]") do
i = i + 1
end
local num_str = input:sub(start, i - 1)
if num_str == "NaN" then
return { type = "number", value = 0 / 0 }
elseif num_str == "Infinity" or num_str == "+Infinity" then
return { type = "number", value = math.huge }
elseif num_str == "-Infinity" then
return { type = "number", value = -math.huge }
else
return { type = "number", value = tonumber(num_str) }
end
end

local function read_identifier()
local start = i
while i <= len and input:sub(i, i):match("[%w%$_]") do
i = i + 1
end
local word = input:sub(start, i - 1)
if word == "true" then
return { type = "boolean", value = true }
elseif word == "false" then
return { type = "boolean", value = false }
elseif word == "null" then
return { type = "null", value = nil }
elseif word == "NaN" or word == "Infinity" or word == "-Infinity" then
i = start
return read_number()
else
return { type = "identifier", value = word }
end
end

while i <= len do
skip_whitespace()
local c = input:sub(i, i)
if c == "" then
break
elseif c == "{" or c == "}" or c == "[" or c == "]" or c == ":" or c == "," then
table.insert(tokens, { type = "symbol", value = c })
i = i + 1
elseif c == '"' or c == "'" then
table.insert(tokens, read_line(c))
elseif c:match("[%+%-%.%d]") then
table.insert(tokens, read_number())
elseif c:match("[%a$_]") then
table.insert(tokens, read_identifier())
else
error("Unexpected character: " .. c)
end
end

return tokens
end

local function parse(tokens)
local pos = 1

local function peek() return tokens[pos] end
local function next_tok()
local token = peek()
pos = pos + 1
return token
end

local function parse_value()
local token = peek()
if not token then error("Unexpected end") end
if token.type == "string" or token.type == "number" or token.type == "boolean" or token.type == "null" then
return next_tok().value
elseif token.value == "{" then
return parse_object()
elseif token.value == "[" then
return parse_array()
elseif token.type == "identifier" then
return next_tok().value
else
error("Unexpected token: " .. token.value)
end
end

function parse_array()
local result = {}
assert(next_tok().value == "[")
while peek() and peek().value ~= "]" do
table.insert(result, parse_value())
if peek().value == "," then next_tok() end
end
assert(next_tok().value == "]")
return result
end

function parse_object()
local result = {}
assert(next_tok().value == "{")
while peek() and peek().value ~= "}" do
local key_token = next_tok()
local key
if key_token.type == "string" or key_token.type == "identifier" then
key = key_token.value
else
error("Invalid object key")
end
assert(next_tok().value == ":")
local val = parse_value()
result[key] = val
if peek().value == "," then next_tok() end
end
assert(next_tok().value == "}")
return result
end

return parse_value()
end

local tokens = tokenize(str)

return parse(tokens)
end


return {
decode = decode,
}
52 changes: 36 additions & 16 deletions lua/lspsettings/loader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ local function set_key(key, value, target)
local head = key:sub(1, index - 1)
local tail = key:sub(index + 1)

target[head] = target[head] or {}
if type(target[head]) ~= "table" then
target[head] = {}
end

set_key(tail, value, target[head])
end
Expand All @@ -59,12 +61,18 @@ end
--- @return string[]
function JsonLoader:list_configured_servers()
local result = {}
local extensions = self.config:extensions(".")

for _, dir in ipairs(self.config.paths) do
for name, type in vim.fs.dir(dir, { follow = true }) do
if type == "file" and name:sub(-5) == ".json" then
local server_name = name:sub(1, -6)
result[server_name] = true
if type == "file" then
for _, ext in ipairs(extensions) do
if name:sub(-ext:len()) == ext then
local server_name = name:sub(1, -(ext:len() + 1))
result[server_name] = true
break
end
end
end
end
end
Expand All @@ -78,10 +86,15 @@ end
function JsonLoader:list_server_configs(server_name)
local result = {}
local paths = self.config.paths
for _, path in ipairs(paths) do
path = vim.fs.joinpath(path, server_name .. ".json")
if vim.fn.filereadable(path) == 1 then
table.insert(result, path)
local fnames = self.config:extensions(server_name .. ".")

for _, dir_path in ipairs(paths) do
for _, fname in ipairs(fnames) do
local file_path = vim.fs.joinpath(dir_path, fname)
if vim.fn.filereadable(file_path) == 1 then
table.insert(result, file_path)
break
end
end
end

Expand All @@ -92,18 +105,25 @@ end
--- @param server_name string
--- @return table
function JsonLoader:load(server_name)
local decode = vim.json.decode
if self.config.json5 then
decode = require("lspsettings.json5").decode
end

local settings = {}

-- reading each config
for _, path in ipairs(self:list_server_configs(server_name)) do
local jsoned = table.concat(vim.fn.readfile(path))
local opts = { luanil = { object = true, array = true } }
local success, data = pcall(vim.json.decode, jsoned, opts)

if success then
settings = vim.tbl_extend("force", settings, data)
else
vim.notify("Unable to load LSP settings at `" .. path .. "`: " .. vim.inspect(data), vim.log.levels.WARN)
local jsoned = table.concat(vim.fn.readfile(path), "\n")
if jsoned ~= "" then
local opts = { luanil = { object = true, array = true } }
local success, data = pcall(decode, jsoned, opts)

if success then
settings = vim.tbl_extend("force", settings, data)
else
vim.notify("Unable to load LSP settings at `" .. path .. "`: invalid JSON", vim.log.levels.WARN)
end
end
end

Expand Down
14 changes: 9 additions & 5 deletions lua/lspsettings/schemas.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Schemas.__index = Schemas
--- @return Schemas
function Schemas:new()
local o = {
schemas = {}
schemas = {},
}

setmetatable(o, self)
Expand Down Expand Up @@ -58,6 +58,7 @@ end

--- Scans `./schemas` directory and forms list of available schemas.
function Schemas:scan()
local extensions = { "json", "json5", "jsonc" }
local init_lua_path = debug.getinfo(1).source:sub(2)
local lspsettings_path = vim.fs.dirname(init_lua_path)
local lua_path = vim.fs.dirname(lspsettings_path)
Expand All @@ -73,9 +74,10 @@ function Schemas:scan()
if type == "file" and name:sub(-5) == ".json" then
local server_name = name:sub(1, -6)
local file_path = vim.fs.abspath(vim.fs.joinpath(schemas_path, name))
local matches = vim.fn.map(extensions, function(_, ext) return server_name .. "." .. ext end)

self.schemas[server_name] = {
fileMatch = { name },
fileMatch = matches,
url = "/" .. file_path,
}
end
Expand All @@ -84,11 +86,13 @@ end

--- Applies given schemas list to `jsonls`
function Schemas:apply()
local config = require("lspsettings").config
local extensions = config:extensions()

vim.lsp.config('jsonls', {
filetypes = extensions,
settings = {
json = {
schemas = vim.tbl_values(self.schemas),
},
json = { schemas = vim.tbl_values(self.schemas) },
},
})
end
Expand Down
1 change: 1 addition & 0 deletions lua/lspsettings/types.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---@class lspsettings.types.config
---@field json5 boolean? default true. Experimental
---@field paths string|string[]?
---@field on_settings fun(server_name: string, settings: table): nil?

Expand Down