Merge pull request #2 from Myzel394/add-lsp-support

Add LSP support
This commit is contained in:
Myzel394 2024-04-14 12:52:29 +02:00 committed by GitHub
commit 4796c4216b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 337 additions and 143 deletions

View File

@ -387,7 +387,7 @@ local function grok_object(self, text, start, options)
---- Add start position so we can quickly jump to it ---- Add start position so we can quickly jump to it
VALUE[key] = { VALUE[key] = {
value = new_val, value = new_val,
newlines = newlines or 0, line_number = (newlines or 0) + 1,
key_start = relative_start + 1, key_start = relative_start + 1,
value_start = get_relative_i(text, i), value_start = get_relative_i(text, i),
} }
@ -440,7 +440,7 @@ local function grok_array(self, text, start, options)
-- can't table.insert(VALUE, val) here because it's a no-op if val is nil -- can't table.insert(VALUE, val) here because it's a no-op if val is nil
VALUE[VALUE_INDEX] = { VALUE[VALUE_INDEX] = {
value = val, value = val,
newlines = newlines or 0, line_number = (newlines or 0) + 1,
value_start = relative_start, value_start = relative_start,
key_start = relative_start, key_start = relative_start,
} }

169
lua/jsonfly/parsers.lua Normal file
View File

@ -0,0 +1,169 @@
---@class EntryPosition
---@field line_number number
---@field key_start number
---@field value_start number
--
---@class Entry
---@field key string
---@field value Entry|table|number|string|boolean|nil
---@field position EntryPosition
--
---@class JSONEntry
---@field value JSONEntry|string|number|boolean|nil
---@field line_number number
---@field value_start number
---@field key_start number
local M = {}
---@param entry JSONEntry
local function get_contents_from_json_value(entry)
local value = entry.value
if type(value) == "table" then
-- Recursively get the contents of the table
local contents = {}
for k, v in pairs(value) do
contents[k] = get_contents_from_json_value(v)
end
return contents
else
return entry.value
end
end
---@param t table
---@return Entry[]
function M:get_entries_from_lua_json(t)
local keys = {}
for k, _raw_value in pairs(t) do
---@type JSONEntry
local raw_value = _raw_value
---@type Entry
local entry = {
key = k,
value = get_contents_from_json_value(raw_value),
position = {
line_number = raw_value.line_number,
key_start = raw_value.key_start,
value_start = raw_value.value_start,
}
}
table.insert(keys, entry)
local v = raw_value.value
if type(v) == "table" then
local sub_keys = M:get_entries_from_lua_json(v)
for _, sub_key in ipairs(sub_keys) do
---@type Entry
local entry = {
key = k .. "." .. sub_key.key,
value = get_contents_from_json_value(sub_key),
position = sub_key.position,
}
table.insert(keys, entry)
end
end
end
return keys
end
---@param result Symbol
---@return string|number|table|boolean|nil
function M:parse_lsp_value(result)
if result.kind == 2 then
local value = {}
for _, child in ipairs(result.children) do
value[child.name] = M:parse_lsp_value(child)
end
return value
elseif result.kind == 16 then
return tonumber(result.detail)
elseif result.kind == 15 then
return result.detail
elseif result.kind == 18 then
local value = {}
for i, child in ipairs(result.children) do
value[i] = M:parse_lsp_value(child)
end
return value
elseif result.kind == 13 then
return nil
elseif result.kind == 17 then
return result.detail == "true"
end
end
---@class Symbol
---@field name string
---@field kind number 2 = Object, 16 = Number, 15 = String, 18 = Array, 13 = Null, 17 = Boolean
---@field range Range
---@field selectionRange Range
---@field detail string
---@field children Symbol[]
--
---@class Range
---@field start Position
---@field ["end"] Position
--
---@class Position
---@field line number
---@field character number
--
---@param symbols Symbol[]
---@return Entry[]
function M:get_entries_from_lsp_symbols(symbols)
local keys = {}
for _, symbol in ipairs(symbols) do
local key = symbol.name
---@type Entry
local entry = {
key = key,
value = M:parse_lsp_value(symbol),
position = {
line_number = symbol.range.start.line + 1,
key_start = symbol.range.start.character + 2,
-- The LSP doesn't return the start of the value, so we'll just assume it's 3 characters after the key
-- We assume a default JSON file like:
-- `"my_key": "my_value"`
-- Since we get the end of the key, we can just add 4 to get the start of the value
value_start = symbol.selectionRange["end"].character + 3,
}
}
table.insert(keys, entry)
if symbol.kind == 2 then
local sub_keys = M:get_entries_from_lsp_symbols(symbol.children)
for _, sub_key in ipairs(sub_keys) do
---@type Entry
local entry = {
key = key .. "." .. sub_key.key,
value = sub_key.value,
position = sub_key.position,
}
table.insert(keys, entry)
end
end
end
return keys
end
return M

64
lua/jsonfly/utils.lua Normal file
View File

@ -0,0 +1,64 @@
local M = {}
function M:truncate_overflow(value, max_length, overflow_marker)
if vim.fn.strdisplaywidth(value) > max_length then
return value:sub(1, max_length - vim.fn.strdisplaywidth(overflow_marker)) .. overflow_marker
end
return value
end
---@param value any
---@param opts Options
function M:create_display_preview(value, opts)
local t = type(value)
local conceal
if opts.conceal == "auto" then
conceal = vim.o.conceallevel > 0
else
conceal = opts.conceal
end
if t == "table" then
local preview_table = {}
for k, v in pairs(value) do
table.insert(preview_table, k .. ": " .. M:create_display_preview(v, opts))
end
return "{ " .. table.concat(preview_table, ", ") .. " }", "other"
elseif t == "nil" then
return "null", "null"
elseif t == "number" then
return tostring(value), "number"
elseif t == "string" then
if conceal then
return value, "string"
else
return "\"" .. value .. "\"", "string"
end
elseif t == "boolean" then
return value and "true" or "false", "boolean"
end
end
---@param key string
---@param replacement string
---@return string
---Replaces all previous keys with the replacement
---Example: replace_previous_keys("a.b.c", "x") => "xxx.c"
function M:replace_previous_keys(key, replacement)
for i = #key, 1, -1 do
if key:sub(i, i) == "." then
local len = i - 1
local before = replacement:rep(len)
return before .. "." .. key:sub(i + 1)
end
end
return key
end
return M

View File

@ -1,3 +1,4 @@
---- Documentation for jsonfly ----
--- Type definitions --- Type definitions
---@class Options ---@class Options
---@field key_max_length number - Length for the key column, 0 for no column-like display, Default: 50 ---@field key_max_length number - Length for the key column, 0 for no column-like display, Default: 50
@ -9,6 +10,7 @@
---@field highlights Highlights - Highlight groups for different types ---@field highlights Highlights - Highlight groups for different types
---@field jump_behavior "key_start"|"value_start" - Behavior for jumping to the location, "key_start" == Jump to the start of the key, "value_start" == Jump to the start of the value, Default: "key_start" ---@field jump_behavior "key_start"|"value_start" - Behavior for jumping to the location, "key_start" == Jump to the start of the key, "value_start" == Jump to the start of the value, Default: "key_start"
---@field subkeys_display "normal"|"waterfall" - Display subkeys in a normal or waterfall style, Default: "normal" ---@field subkeys_display "normal"|"waterfall" - Display subkeys in a normal or waterfall style, Default: "normal"
---@field backend "lua"|"lsp" - Backend to use for parsing JSON, "lua" = Use our own Lua parser to parse the JSON, "lsp" = Use your LSP to parse the JSON (currently only https://github.com/Microsoft/vscode-json-languageservice is supported). If the "lsp" backend is selected but the LSP fails, it will fallback to the "lua" backend, Default: "lsp"
--- ---
---@class Highlights ---@class Highlights
---@field number string - Highlight group for numbers, Default: "@number.json" ---@field number string - Highlight group for numbers, Default: "@number.json"
@ -17,93 +19,17 @@
---@field null string - Highlight group for null values, Default: "@constant.builtin.json" ---@field null string - Highlight group for null values, Default: "@constant.builtin.json"
---@field other string - Highlight group for other types, Default: "@label.json" ---@field other string - Highlight group for other types, Default: "@label.json"
local parsers = require"jsonfly.parsers"
local json = require"jsonfly.json"
local utils = require"jsonfly.utils"
local json = require"jsonfly.json" local json = require"jsonfly.json"
local finders = require "telescope.finders" local finders = require "telescope.finders"
local pickers = require "telescope.pickers" local pickers = require "telescope.pickers"
local conf = require("telescope.config").values local conf = require"telescope.config".values
local make_entry = require "telescope.make_entry" local make_entry = require "telescope.make_entry"
local entry_display = require "telescope.pickers.entry_display" local entry_display = require "telescope.pickers.entry_display"
local function get_recursive_keys(t)
local keys = {}
for k, raw_value in pairs(t) do
table.insert(keys, {key = k, entry = raw_value})
local v = raw_value.value
if type(v) == "table" then
local sub_keys = get_recursive_keys(v)
for _, sub_key in ipairs(sub_keys) do
table.insert(keys, {key = k .. "." .. sub_key.key, entry = sub_key.entry})
end
end
end
return keys
end
local function truncate_overflow(value, max_length, overflow_marker)
if vim.fn.strdisplaywidth(value) > max_length then
return value:sub(1, max_length - vim.fn.strdisplaywidth(overflow_marker)) .. overflow_marker
end
return value
end
---@param value any
---@param opts Options
local function create_display_preview(value, opts)
local t = type(value)
local conceal
if opts.conceal == "auto" then
conceal = vim.o.conceallevel > 0
else
conceal = opts.conceal
end
if t == "table" then
local preview_table = {}
for k, v in pairs(value) do
table.insert(preview_table, k .. ": " .. create_display_preview(v.value, opts))
end
return "{ " .. table.concat(preview_table, ", ") .. " }", "other"
elseif t == "nil" then
return "null", "null"
elseif t == "number" then
return tostring(value), "number"
elseif t == "string" then
if conceal then
return value, "string"
else
return "\"" .. value .. "\"", "string"
end
elseif t == "boolean" then
return value and "true" or "false", "boolean"
end
end
---@param key string
---@param replacement string
---@return string
---Replaces all previous keys with the replacement
---Example: replace_previous_keys("a.b.c", "x") => "xxx.c"
local function replace_previous_keys(key, replacement)
for i = #key, 1, -1 do
if key:sub(i, i) == "." then
local len = i - 1
local before = replacement:rep(len)
return before .. "." .. key:sub(i + 1)
end
end
return key
end
---@type Options ---@type Options
local opts = { local opts = {
key_max_length = 50, key_max_length = 50,
@ -121,82 +47,117 @@ local opts = {
}, },
jump_behavior = "key_start", jump_behavior = "key_start",
subkeys_display = "normal", subkeys_display = "normal",
backend = "lsp",
} }
---@param results Entry[]
---@param buffer number
local function show_picker(results, buffer)
local filename = vim.api.nvim_buf_get_name(buffer)
local displayer = entry_display.create {
separator = " ",
items = {
{ width = 1 },
opts.key_exact_length and { width = opts.key_max_length } or { remaining = true },
{ remaining = true },
},
}
pickers.new(opts, {
prompt_title = opts.prompt_title,
finder = finders.new_table {
results = results,
---@param entry Entry
entry_maker = function(entry)
local _, raw_depth = entry.key:gsub("%.", ".")
local depth = (raw_depth or 0) + 1
return make_entry.set_default_entry_mt({
value = buffer,
ordinal = entry.key,
display = function(_)
local preview, hl_group_key = utils:create_display_preview(entry.value, opts)
local key = opts.subkeys_display == "normal" and entry.key or utils:replace_previous_keys(entry.key, " ")
print(vim.inspect(entry))
return displayer {
{ depth, "TelescopeResultsNumber"},
{
utils:truncate_overflow(
key,
opts.key_max_length,
opts.overflow_marker
),
"@property.json",
},
{
utils:truncate_overflow(
preview,
opts.max_length,
opts.overflow_marker
),
opts.highlights[hl_group_key] or "TelescopeResultsString",
},
}
end,
bufnr = buffer,
filename = filename,
lnum = entry.position.line_number,
col = opts.jump_behavior == "key_start"
and entry.position.key_start
-- Use length ("#" operator) as vim jumps to the bytes, not characters
or entry.position.value_start
}, opts)
end,
},
previewer = conf.grep_previewer(opts),
sorter = conf.generic_sorter(opts),
sorting_strategy = "ascending",
}):find()
end
return require"telescope".register_extension { return require"telescope".register_extension {
setup = function(extension_config) setup = function(extension_config)
opts = vim.tbl_deep_extend("force", opts, extension_config or {}) opts = vim.tbl_deep_extend("force", opts, extension_config or {})
end, end,
exports = { exports = {
jsonfly = function() jsonfly = function(xopts)
local current_buf = vim.api.nvim_get_current_buf() local current_buf = vim.api.nvim_get_current_buf()
local filename = vim.api.nvim_buf_get_name(current_buf)
local content_lines = vim.api.nvim_buf_get_lines(current_buf, 0, -1, false) local content_lines = vim.api.nvim_buf_get_lines(current_buf, 0, -1, false)
local content = table.concat(content_lines, "\n") local content = table.concat(content_lines, "\n")
local parsed = json:decode(content) function run_lua_parser()
local keys = get_recursive_keys(parsed) local parsed = json:decode(content)
local keys = parsers:get_entries_from_lua_json(parsed)
local displayer = entry_display.create { show_picker(keys, current_buf)
separator = " ", end
items = {
{ width = 1 },
opts.key_exact_length and { width = opts.key_max_length } or { remaining = true },
{ remaining = true },
},
}
pickers.new(opts, { if opts.backend == "lsp" then
prompt_title = opts.prompt_title, local params = vim.lsp.util.make_position_params(xopts.winnr)
finder = finders.new_table {
results = keys,
entry_maker = function(entry)
local _, raw_depth = entry.key:gsub("%.", ".")
local depth = (raw_depth or 0) + 1
return make_entry.set_default_entry_mt({ vim.lsp.buf_request(
value = current_buf, current_buf,
ordinal = entry.key, "textDocument/documentSymbol",
display = function(_) params,
local preview, hl_group_key = create_display_preview(entry.entry.value, opts) function(error, lsp_response)
if error then
run_lua_parser()
return
end
local key = opts.subkeys_display == "normal" and entry.key or replace_previous_keys(entry.key, " ") local result = parsers:get_entries_from_lsp_symbols(lsp_response)
return displayer { show_picker(result, current_buf)
{ depth, "TelescopeResultsNumber"}, end
{ )
truncate_overflow( else
key, run_lua_parser()
opts.key_max_length, end
opts.overflow_marker
),
"@property.json",
},
{
truncate_overflow(
preview,
opts.max_length,
opts.overflow_marker
),
opts.highlights[hl_group_key] or "TelescopeResultsString",
},
}
end,
bufnr = current_buf,
filename = filename,
lnum = entry.entry.newlines + 1,
col = opts.jump_behavior == "key_start"
and entry.entry.key_start
-- Use length ("#" operator) as vim jumps to the bytes, not characters
or entry.entry.value_start
}, opts)
end,
},
previewer = conf.grep_previewer(opts),
sorter = conf.generic_sorter(opts),
sorting_strategy = "ascending",
}):find()
end end
} }
} }