Compare commits

...

6 Commits

Author SHA1 Message Date
2346a75cae
feat(dev): Add flake.nix 2025-06-06 12:55:07 +02:00
0e5158a8d4
chore: apply stylua 2025-06-06 12:54:16 +02:00
895e9adfdf
fix: merge main 2025-06-06 12:43:58 +02:00
Myzel394
d713d9ef38
Merge pull request #20 from kletobias/custom-jsonfly
Fix: Handle Dictionary Responses in jsonfly.lua for Neovim 0.10.1
2024-09-30 16:22:17 +02:00
Myzel394
8fe79c2783
fix: Fix fallback behavior; apply stylua 2024-09-30 16:18:19 +02:00
Tobias Klein
cbff2a5659 fix(jsonfly.lua): resolve function buf_request_all to work with new nvim
syntax and version changes not accounted for.
Refactored LSP request handling in jsonfly.lua to combine results from multiple LSP responses and handle empty results more gracefully. Improved code readability and maintainability by restructuring the nested if-else blocks.
2024-09-29 15:55:36 +02:00
9 changed files with 1286 additions and 1168 deletions

61
flake.lock generated Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1748929857,
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

43
flake.nix Normal file
View File

@ -0,0 +1,43 @@
{
description = "jsonfly";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, utils, ... } @ inputs:
utils.lib.eachDefaultSystem(system:
let
pkgs = nixpkgs.legacyPackages.${system};
logo = pkgs.writeText "logo.txt" ''
'';
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
stylua
# If this ever fails, just remove it. It's just for the logo.
lolcat
];
shellHook = ''
cat ${logo} | lolcat
echo "";
echo "Welcome to the jsonfly.nvim development environment!";
echo "";
'';
};
}
);
}

10
justfile Normal file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env just --justfile
set dotenv-load := true
_default:
just --list -u
lint:
stylua lua

View File

@ -1,47 +1,42 @@
local M = {};
local M = {}
local _cache = {};
local _cache = {}
---@param buffer integer
function M:cache_buffer(buffer, value)
_cache[buffer] = value;
_cache[buffer] = value
end
---@param buffer integer
function M:invalidate_buffer(buffer)
_cache[buffer] = nil;
_cache[buffer] = nil
end
---@param buffer integer
---@return string[]|nil
function M:get_cache(buffer)
return _cache[buffer];
return _cache[buffer]
end
local _listening_buffers = {};
local _listening_buffers = {}
---@param buffer integer
function M:register_listeners(buffer)
if _listening_buffers[buffer] then
return;
end
if _listening_buffers[buffer] then
return
end
_listening_buffers[buffer] = true;
_listening_buffers[buffer] = true
vim.api.nvim_buf_attach(
buffer,
false,
{
on_lines = function()
self:invalidate_buffer(buffer)
end,
on_detach = function()
self:invalidate_buffer(buffer)
_listening_buffers[buffer] = nil;
end,
}
);
vim.api.nvim_buf_attach(buffer, false, {
on_lines = function()
self:invalidate_buffer(buffer)
end,
on_detach = function()
self:invalidate_buffer(buffer)
_listening_buffers[buffer] = nil
end,
})
end
return M;
return M

View File

@ -1,42 +1,42 @@
local utils = require"jsonfly.utils"
local utils = require("jsonfly.utils")
-- This string will be used to position the cursor properly.
-- Once everything is set, the cursor searches for this string and jumps to it.
-- After that, it will be removed immediately.
local CURSOR_SEARCH_HELPER = "_jsonFfFfFfLyY0904857CursorHelperRrRrRrR"
local M = {};
local M = {}
-- https://stackoverflow.com/a/24823383/9878135
function table.slice(tbl, first, last, step)
local sliced = {}
local sliced = {}
for i = first or 1, last or #tbl, step or 1 do
sliced[#sliced+1] = tbl[i]
end
for i = first or 1, last or #tbl, step or 1 do
sliced[#sliced + 1] = tbl[i]
end
return sliced
return sliced
end
---@param line string
---@param also_match_end_bracket boolean - Whether to also match only a closing bracket
---@return boolean - Whether the line contains an empty JSON object
local function line_contains_empty_json(line, also_match_end_bracket)
-- Starting and ending on same line
return string.match(line, ".*[%{%[]%s*[%}%]]%s*,?*%s*")
-- Opening bracket on line
or string.match(line, ".*[%{%[]%s*")
-- Closing bracket on line
or (also_match_end_bracket and string.match(line, ".*.*[%}%]]%s*,?%s*"))
-- Starting and ending on same line
return string.match(line, ".*[%{%[]%s*[%}%]]%s*,?*%s*")
-- Opening bracket on line
or string.match(line, ".*[%{%[]%s*")
-- Closing bracket on line
or (also_match_end_bracket and string.match(line, ".*.*[%}%]]%s*,?%s*"))
end
---@param entry Entry
---@param key string
---@param index number
local function check_key_equal(entry, key, index)
local splitted = utils:split_by_char(entry.key, ".")
local splitted = utils:split_by_char(entry.key, ".")
return splitted[index] == key
return splitted[index] == key
end
---Find the entry in `entries` with the most matching keys at the beginning based on the `keys`.
@ -45,214 +45,196 @@ end
---@param keys string[]
---@return number|nil
local function find_best_fitting_entry(entries, keys)
local entry_index
local current_indexes = {1, #entries}
local entry_index
local current_indexes = { 1, #entries }
for kk=1, #keys do
local key = keys[kk]
for kk = 1, #keys do
local key = keys[kk]
local start_index = current_indexes[1]
local end_index = current_indexes[2]
local start_index = current_indexes[1]
local end_index = current_indexes[2]
current_indexes = {nil, nil}
current_indexes = { nil, nil }
for ii=start_index, end_index do
if check_key_equal(entries[ii], key, kk) then
if current_indexes[1] == nil then
current_indexes[1] = ii
end
for ii = start_index, end_index do
if check_key_equal(entries[ii], key, kk) then
if current_indexes[1] == nil then
current_indexes[1] = ii
end
current_indexes[2] = ii
end
end
current_indexes[2] = ii
end
end
if current_indexes[1] == nil then
-- No entries found
break
else
entry_index = current_indexes[1]
end
end
if current_indexes[1] == nil then
-- No entries found
break
else
entry_index = current_indexes[1]
end
end
return entry_index
return entry_index
end
---@param keys KeyDescription
---@param index number - Index of the key
---@param lines string[] - Table to write the lines to
local function write_keys(keys, index, lines)
local key = keys[index]
local key = keys[index]
if index == #keys then
lines[#lines + 1] = "\"" .. key.key .. "\": \"" .. CURSOR_SEARCH_HELPER .. "\""
return
end
if index == #keys then
lines[#lines + 1] = '"' .. key.key .. '": "' .. CURSOR_SEARCH_HELPER .. '"'
return
end
if key.type == "object_wrapper" then
local previous_line = lines[#lines] or ""
if line_contains_empty_json(previous_line, true) or #lines == 0 then
lines[#lines + 1] = "{"
else
lines[#lines] = previous_line .. " {"
end
if key.type == "object_wrapper" then
local previous_line = lines[#lines] or ""
if line_contains_empty_json(previous_line, true) or #lines == 0 then
lines[#lines + 1] = "{"
else
lines[#lines] = previous_line .. " {"
end
write_keys(keys, index + 1, lines)
write_keys(keys, index + 1, lines)
lines[#lines + 1] = "}"
elseif key.type == "key" then
lines[#lines + 1] = "\"" .. key.key .. "\":"
lines[#lines + 1] = "}"
elseif key.type == "key" then
lines[#lines + 1] = '"' .. key.key .. '":'
write_keys(keys, index + 1, lines)
elseif key.type == "array_wrapper" then
local previous_line = lines[#lines] or ""
-- Starting and ending on same line
if line_contains_empty_json(previous_line, true) or #lines == 0 then
lines[#lines + 1] = "["
else
lines[#lines] = previous_line .. " ["
end
write_keys(keys, index + 1, lines)
write_keys(keys, index + 1, lines)
elseif key.type == "array_wrapper" then
local previous_line = lines[#lines] or ""
-- Starting and ending on same line
if line_contains_empty_json(previous_line, true) or #lines == 0 then
lines[#lines + 1] = "["
else
lines[#lines] = previous_line .. " ["
end
write_keys(keys, index + 1, lines)
lines[#lines + 1] = "]"
elseif key.type == "array_index" then
local amount = tonumber(key.key)
-- Write previous empty array objects
for _=1, amount do
lines[#lines + 1] = "{},"
end
lines[#lines + 1] = "]"
elseif key.type == "array_index" then
local amount = tonumber(key.key)
-- Write previous empty array objects
for _ = 1, amount do
lines[#lines + 1] = "{},"
end
write_keys(keys, index + 1, lines)
end
write_keys(keys, index + 1, lines)
end
end
---@param buffer number
---@param insertion_line number
local function add_comma(buffer, insertion_line)
local BUFFER_SIZE = 5
local BUFFER_SIZE = 5
-- Find next non-empty character in reverse
for ii=insertion_line, 0, -BUFFER_SIZE do
local previous_lines = vim.api.nvim_buf_get_lines(
buffer,
math.max(0, ii - BUFFER_SIZE),
ii,
false
)
-- Find next non-empty character in reverse
for ii = insertion_line, 0, -BUFFER_SIZE do
local previous_lines = vim.api.nvim_buf_get_lines(buffer, math.max(0, ii - BUFFER_SIZE), ii, false)
if #previous_lines == 0 then
return
end
if #previous_lines == 0 then
return
end
for jj=#previous_lines, 1, -1 do
local line = previous_lines[jj]
for jj = #previous_lines, 1, -1 do
local line = previous_lines[jj]
for char_index=#line, 1, -1 do
local char = line:sub(char_index, char_index)
for char_index = #line, 1, -1 do
local char = line:sub(char_index, char_index)
if char ~= " " and char ~= "\t" and char ~= "\n" and char ~= "\r" then
if char == "," or char == "{" or char == "[" then
return
end
if char ~= " " and char ~= "\t" and char ~= "\n" and char ~= "\r" then
if char == "," or char == "{" or char == "[" then
return
end
-- Insert comma at position
local line_number = math.max(0, ii - BUFFER_SIZE) + jj - 1
vim.api.nvim_buf_set_text(
buffer,
line_number,
char_index,
line_number,
char_index,
{","}
)
return
end
end
end
end
-- Insert comma at position
local line_number = math.max(0, ii - BUFFER_SIZE) + jj - 1
vim.api.nvim_buf_set_text(buffer, line_number, char_index, line_number, char_index, { "," })
return
end
end
end
end
end
---@return number - The new line number to be used, as the buffer has been modified
local function expand_empty_object(buffer, line_number)
local line = vim.api.nvim_buf_get_lines(buffer, line_number, line_number + 1, false)[1] or ""
local line = vim.api.nvim_buf_get_lines(buffer, line_number, line_number + 1, false)[1] or ""
if line_contains_empty_json(line, false) then
local position_closing_bracket = string.find(line, "[%}%]]")
local remaining_line = string.sub(line, position_closing_bracket + 1)
if line_contains_empty_json(line, false) then
local position_closing_bracket = string.find(line, "[%}%]]")
local remaining_line = string.sub(line, position_closing_bracket + 1)
vim.api.nvim_buf_set_lines(
buffer,
line_number,
line_number + 1,
false,
{
"{",
"}" .. remaining_line
}
)
vim.api.nvim_buf_set_lines(buffer, line_number, line_number + 1, false, {
"{",
"}" .. remaining_line,
})
return line_number + 1
end
return line_number + 1
end
return line_number
return line_number
end
---@param keys KeyDescription[]
---@param input_key_depth number
local function get_key_descriptor_index(keys, input_key_depth)
local depth = 0
local index = 0
local depth = 0
local index = 0
for ii=1, #keys do
if keys[ii].type == "key" or keys[ii].type == "array_index" then
depth = depth + 1
end
for ii = 1, #keys do
if keys[ii].type == "key" or keys[ii].type == "array_index" then
depth = depth + 1
end
if depth >= input_key_depth then
index = ii
break
end
end
if depth >= input_key_depth then
index = ii
break
end
end
return index
return index
end
---@param entries Entry[]
---@param keys string[]
---@return integer|nil - The index of the entry
local function get_entry_by_keys(entries, keys)
for ii=1, #entries do
local entry = entries[ii]
local splitted = utils:split_by_char(entry.key, ".")
for ii = 1, #entries do
local entry = entries[ii]
local splitted = utils:split_by_char(entry.key, ".")
local found = true
local found = true
for jj=1, #keys do
if keys[jj] ~= splitted[jj] then
found = false
break
end
end
for jj = 1, #keys do
if keys[jj] ~= splitted[jj] then
found = false
break
end
end
if found then
return ii
end
end
if found then
return ii
end
end
end
---@param keys KeyDescription[]
---@return string[]
local function flatten_key_description(keys)
local flat_keys = {}
local flat_keys = {}
for ii=1, #keys do
if keys[ii].type == "key" then
flat_keys[#flat_keys + 1] = keys[ii].key
elseif keys[ii].type == "array_index" then
flat_keys[#flat_keys + 1] = tostring(keys[ii].key)
end
end
for ii = 1, #keys do
if keys[ii].type == "key" then
flat_keys[#flat_keys + 1] = keys[ii].key
elseif keys[ii].type == "array_index" then
flat_keys[#flat_keys + 1] = tostring(keys[ii].key)
end
end
return flat_keys
return flat_keys
end
---Subtracts indexes if there are other indexes before already
@ -262,43 +244,43 @@ end
---@param starting_keys KeyDescription[]
---@param key KeyDescription - Th key to be inserted; must be of type `array_index`; will be modified in-place
local function normalize_array_indexes(entries, starting_keys, key)
local starting_keys_flat = flatten_key_description(starting_keys)
local starting_key_index = get_entry_by_keys(entries, starting_keys_flat)
local entry = entries[starting_key_index]
local starting_keys_flat = flatten_key_description(starting_keys)
local starting_key_index = get_entry_by_keys(entries, starting_keys_flat)
local entry = entries[starting_key_index]
key.key = key.key - #entry.value
key.key = key.key - #entry.value
end
---@param entries Entry[] - Entries, they must be children of a top level array
---Counts how many top level children an array has
local function count_array_children(entries)
for ii=1, #entries do
if string.match(entries[ii].key, "^%d+$") then
return ii
end
end
for ii = 1, #entries do
if string.match(entries[ii].key, "^%d+$") then
return ii
end
end
return #entries
return #entries
end
---Jump to the cursor helper and remove it
---@param buffer number
function M:jump_to_cursor_helper(buffer)
vim.fn.search(CURSOR_SEARCH_HELPER)
vim.fn.search(CURSOR_SEARCH_HELPER)
-- Remove cursor helper
local position = vim.api.nvim_win_get_cursor(0)
vim.api.nvim_buf_set_text(
buffer,
position[1] - 1,
position[2],
position[1] - 1,
position[2] + #CURSOR_SEARCH_HELPER,
{""}
)
-- Remove cursor helper
local position = vim.api.nvim_win_get_cursor(0)
vim.api.nvim_buf_set_text(
buffer,
position[1] - 1,
position[2],
position[1] - 1,
position[2] + #CURSOR_SEARCH_HELPER,
{ "" }
)
-- -- Go into insert mode
vim.cmd [[execute "normal a"]]
-- -- Go into insert mode
vim.cmd([[execute "normal a"]])
end
-- TODO: Handle top level empty arrays
@ -306,107 +288,107 @@ end
---@param keys KeyDescription[]
---@param buffer number
function M:insert_new_key(entries, keys, buffer)
-- Close current buffer
vim.cmd [[quit!]]
-- Close current buffer
vim.cmd([[quit!]])
local input_key = flatten_key_description(keys)
---@type boolean
local should_add_comma = true
local input_key = flatten_key_description(keys)
---@type boolean
local should_add_comma = true
---@type KeyDescription[]
local remaining_keys
---@type Entry
local entry
---@type KeyDescription[]
local remaining_keys
---@type Entry
local entry
if #entries == 0 then
remaining_keys = table.slice(keys, 2, #keys)
if #entries == 0 then
remaining_keys = table.slice(keys, 2, #keys)
entry = {
key = "",
position = {
key_start = 1,
line_number = 1,
value_start = 1
}
}
should_add_comma = false
else
local entry_index = find_best_fitting_entry(entries, input_key) or 0
entry = entries[entry_index]
entry = {
key = "",
position = {
key_start = 1,
line_number = 1,
value_start = 1,
},
}
should_add_comma = false
else
local entry_index = find_best_fitting_entry(entries, input_key) or 0
entry = entries[entry_index]
---@type integer
local existing_keys_index
---@type integer
local existing_keys_index
if entry == nil then
-- Insert as root
existing_keys_index = 0
remaining_keys = table.slice(keys, 2, #keys)
if entry == nil then
-- Insert as root
existing_keys_index = 0
remaining_keys = table.slice(keys, 2, #keys)
-- Top level array
if entries[1].key == "0" then
-- Normalize array indexes
remaining_keys[1].key = remaining_keys[1].key - count_array_children(entries)
end
-- Top level array
if entries[1].key == "0" then
-- Normalize array indexes
remaining_keys[1].key = remaining_keys[1].key - count_array_children(entries)
end
entry = {
key = "",
position = {
key_start = 1,
line_number = 1,
value_start = 1
}
}
else
local existing_input_keys_depth = #utils:split_by_char(entry.key, ".") + 1
existing_keys_index = get_key_descriptor_index(keys, existing_input_keys_depth)
remaining_keys = table.slice(keys, existing_keys_index, #keys)
entry = {
key = "",
position = {
key_start = 1,
line_number = 1,
value_start = 1,
},
}
else
local existing_input_keys_depth = #utils:split_by_char(entry.key, ".") + 1
existing_keys_index = get_key_descriptor_index(keys, existing_input_keys_depth)
remaining_keys = table.slice(keys, existing_keys_index, #keys)
if remaining_keys[1].type == "array_index" then
local starting_keys = table.slice(keys, 1, existing_keys_index - 1)
normalize_array_indexes(entries, starting_keys, remaining_keys[1])
end
end
end
if remaining_keys[1].type == "array_index" then
local starting_keys = table.slice(keys, 1, existing_keys_index - 1)
normalize_array_indexes(entries, starting_keys, remaining_keys[1])
end
end
end
local _writes = {}
write_keys(remaining_keys, 1, _writes)
local writes = {}
local _writes = {}
write_keys(remaining_keys, 1, _writes)
local writes = {}
for ii=1, #_writes do
if _writes[ii] == true then
-- Unwrap table
writes[#writes] = writes[#writes][1]
else
writes[#writes + 1] = _writes[ii]
end
end
for ii = 1, #_writes do
if _writes[ii] == true then
-- Unwrap table
writes[#writes] = writes[#writes][1]
else
writes[#writes + 1] = _writes[ii]
end
end
-- Hacky way to jump to end of object
vim.api.nvim_win_set_cursor(0, {entry.position.line_number, entry.position.value_start})
vim.cmd [[execute "normal %"]]
-- Hacky way to jump to end of object
vim.api.nvim_win_set_cursor(0, { entry.position.line_number, entry.position.value_start })
vim.cmd([[execute "normal %"]])
local changes = #writes
local start_line = vim.api.nvim_win_get_cursor(0)[1] - 1
local changes = #writes
local start_line = vim.api.nvim_win_get_cursor(0)[1] - 1
-- Add comma to previous JSON entry
if should_add_comma then
add_comma(buffer, start_line)
end
local new_start_line = expand_empty_object(buffer, start_line)
-- Add comma to previous JSON entry
if should_add_comma then
add_comma(buffer, start_line)
end
local new_start_line = expand_empty_object(buffer, start_line)
if new_start_line ~= start_line then
changes = changes + math.abs(new_start_line - start_line)
start_line = new_start_line
end
if new_start_line ~= start_line then
changes = changes + math.abs(new_start_line - start_line)
start_line = new_start_line
end
-- Insert new lines
vim.api.nvim_buf_set_lines(buffer, start_line, start_line, false, writes)
-- Insert new lines
vim.api.nvim_buf_set_lines(buffer, start_line, start_line, false, writes)
-- Format lines
vim.api.nvim_win_set_cursor(0, {start_line, 1})
vim.cmd('execute "normal =' .. changes .. 'j"')
-- Format lines
vim.api.nvim_win_set_cursor(0, { start_line, 1 })
vim.cmd('execute "normal =' .. changes .. 'j"')
M:jump_to_cursor_helper(buffer)
M:jump_to_cursor_helper(buffer)
end
return M;
return M

File diff suppressed because it is too large Load Diff

View File

@ -15,121 +15,120 @@
---@field key_start number
local PRIMITIVE_TYPES = {
string = true,
number = true,
boolean = true,
string = true,
number = true,
boolean = true,
}
local CONTAINS_CHILDREN_TYPES = {
[2] = true, -- Module / Javascript Object
[8] = true, -- Field
[18] = true, -- Array
[19] = true, -- Object
[2] = true, -- Module / Javascript Object
[8] = true, -- Field
[18] = true, -- Array
[19] = true, -- Object
}
local M = {}
---@param entry JSONEntry
local function get_contents_from_json_value(entry)
local value = entry.value
local value = entry.value
if type(value) == "table" then
-- Recursively get the contents of the table
local contents = {}
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
for k, v in pairs(value) do
contents[k] = get_contents_from_json_value(v)
end
return contents
else
return entry.value
end
return contents
else
return entry.value
end
end
---@param t table|nil|string|number|boolean
---@return Entry[]
function M:get_entries_from_lua_json(t)
if PRIMITIVE_TYPES[type(t)] or t == nil then
return {}
end
if PRIMITIVE_TYPES[type(t)] or t == nil then
return {}
end
local keys = {}
local keys = {}
for k, _raw_value in pairs(t) do
---@type JSONEntry
local raw_value = _raw_value
---@type Entry
local entry = {
key = tostring(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)
for k, _raw_value in pairs(t) do
---@type JSONEntry
local raw_value = _raw_value
---@type Entry
local entry = {
key = tostring(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
local v = raw_value.value
if type(v) == "table" then
local sub_keys = M:get_entries_from_lua_json(v)
if type(v) == "table" then
local sub_keys = M:get_entries_from_lua_json(v)
for index=1, #sub_keys do
local sub_key = sub_keys[index]
for index = 1, #sub_keys do
local sub_key = sub_keys[index]
---@type Entry
local entry = {
key = k .. "." .. sub_key.key,
value = sub_key.value,
position = sub_key.position,
}
---@type Entry
local entry = {
key = k .. "." .. sub_key.key,
value = sub_key.value,
position = sub_key.position,
}
keys[#keys + 1] = entry
end
end
end
keys[#keys + 1] = entry
end
end
end
return keys
return keys
end
---@param result Symbol
---@return string|number|table|boolean|nil
function M:parse_lsp_value(result)
-- Object
if CONTAINS_CHILDREN_TYPES[result.kind] then
local value = {}
-- Object
if CONTAINS_CHILDREN_TYPES[result.kind] then
local value = {}
for _, child in ipairs(result.children) do
value[child.name] = M:parse_lsp_value(child)
end
for _, child in ipairs(result.children) do
value[child.name] = M:parse_lsp_value(child)
end
return value
-- Integer
elseif result.kind == 16 then
return tonumber(result.detail)
-- String
elseif result.kind == 15 then
return result.detail
-- Array
elseif result.kind == 18 then
local value = {}
return value
-- Integer
elseif result.kind == 16 then
return tonumber(result.detail)
-- String
elseif result.kind == 15 then
return result.detail
-- Array
elseif result.kind == 18 then
local value = {}
for i, child in ipairs(result.children) do
value[i] = M:parse_lsp_value(child)
end
for i, child in ipairs(result.children) do
value[i] = M:parse_lsp_value(child)
end
return value
-- null
elseif result.kind == 13 then
return nil
-- boolean
elseif result.kind == 17 then
return result.detail == "true"
end
return value
-- null
elseif result.kind == 13 then
return nil
-- boolean
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
@ -149,45 +148,45 @@ end
---@param symbols Symbol[]
---@return Entry[]
function M:get_entries_from_lsp_symbols(symbols)
local keys = {}
local keys = {}
for index=1, #symbols do
local symbol = symbols[index]
local key = symbol.name
for index = 1, #symbols do
local symbol = symbols[index]
local key = symbol.name
---@type Entry
local entry = {
key = tostring(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,
}
}
keys[#keys + 1] = entry
---@type Entry
local entry = {
key = tostring(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,
},
}
keys[#keys + 1] = entry
if CONTAINS_CHILDREN_TYPES[symbol.kind] then
local sub_keys = M:get_entries_from_lsp_symbols(symbol.children)
if CONTAINS_CHILDREN_TYPES[symbol.kind] then
local sub_keys = M:get_entries_from_lsp_symbols(symbol.children)
for jindex=1, #sub_keys do
---@type Entry
local entry = {
key = key .. "." .. sub_keys[jindex].key,
value = sub_keys[jindex].value,
position = sub_keys[jindex].position,
}
for jindex = 1, #sub_keys do
---@type Entry
local entry = {
key = key .. "." .. sub_keys[jindex].key,
value = sub_keys[jindex].value,
position = sub_keys[jindex].position,
}
keys[#keys + 1] = entry
end
end
end
keys[#keys + 1] = entry
end
end
end
return keys
return keys
end
return M

View File

@ -54,43 +54,43 @@
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
if vim.fn.strdisplaywidth(value) > max_length then
return value:sub(1, max_length - vim.fn.strdisplaywidth(overflow_marker)) .. overflow_marker
end
return value
return value
end
---@param value any
---@param conceal boolean
---@param render_objects boolean
function M:create_display_preview(value, conceal, render_objects)
local t = type(value)
local t = type(value)
if t == "table" then
if render_objects == false then
return "", "other"
end
local preview_table = {}
if t == "table" then
if render_objects == false then
return "", "other"
end
local preview_table = {}
for k, v in pairs(value) do
preview_table[#preview_table + 1] = k .. ": " .. M:create_display_preview(v, conceal, render_objects)
end
for k, v in pairs(value) do
preview_table[#preview_table + 1] = k .. ": " .. M:create_display_preview(v, conceal, render_objects)
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
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
@ -99,116 +99,118 @@ end
---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)
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 before .. "." .. key:sub(i + 1)
end
end
return key
return key
end
---@param text string
---@param char string
---@return string[]
function M:split_by_char(text, char)
local parts = {}
local current = ""
local parts = {}
local current = ""
for i = 1, #text do
local c = text:sub(i, i)
for i = 1, #text do
local c = text:sub(i, i)
if c == char then
parts[#parts + 1] = current
current = ""
else
current = current .. c
end
end
if c == char then
parts[#parts + 1] = current
current = ""
else
current = current .. c
end
end
parts[#parts + 1] = current
parts[#parts + 1] = current
return parts
return parts
end
---@param text string
---@return KeyDescription[]
function M:extract_key_description(text)
local keys = {}
local keys = {}
local splitted = M:split_by_char(text, ".")
local splitted = M:split_by_char(text, ".")
local index = 1
local index = 1
while index <= #splitted do
local token = splitted[index]
while index <= #splitted do
local token = splitted[index]
-- Escape
if string.sub(token, 1, 1) == "\\" then
token = token:sub(2)
-- Escape
if string.sub(token, 1, 1) == "\\" then
token = token:sub(2)
keys[#keys + 1] = {
type = "object_wrapper",
}
keys[#keys + 1] = {
key = token,
type = "key",
}
-- Array
elseif string.match(token, "%[%d+%]") then
local array_index = tonumber(string.sub(token, 2, -2))
keys[#keys + 1] = {
type = "object_wrapper",
}
keys[#keys + 1] = {
key = token,
type = "key",
}
-- Array
elseif string.match(token, "%[%d+%]") then
local array_index = tonumber(string.sub(token, 2, -2))
keys[#keys + 1] = {
type = "array_wrapper",
}
keys[#keys + 1] = {
key = array_index,
type = "array_index",
}
-- Array
elseif string.match(token, "%d+") then
local array_index = tonumber(token)
keys[#keys + 1] = {
type = "array_wrapper",
}
keys[#keys + 1] = {
key = array_index,
type = "array_index",
}
-- Array
elseif string.match(token, "%d+") then
local array_index = tonumber(token)
keys[#keys + 1] = {
type = "array_wrapper",
}
keys[#keys + 1] = {
key = array_index,
type = "array_index",
}
-- Object
else
keys[#keys + 1] = {
type = "object_wrapper",
}
keys[#keys + 1] = {
key = token,
type = "key",
}
end
keys[#keys + 1] = {
type = "array_wrapper",
}
keys[#keys + 1] = {
key = array_index,
type = "array_index",
}
-- Object
else
keys[#keys + 1] = {
type = "object_wrapper",
}
keys[#keys + 1] = {
key = token,
type = "key",
}
end
index = index + 1
end
index = index + 1
end
if #keys == 0 then
return {
{
key = text,
type = "key",
}
}
end
if #keys == 0 then
return {
{
key = text,
type = "key",
},
}
end
return keys
return keys
end
---@param name string
---@return boolean
function M:is_module_available(name)
return pcall(function() require(name) end) == true
return pcall(function()
require(name)
end) == true
end
return M

View File

@ -26,52 +26,52 @@
---@field null string - Highlight group for null values, Default: "@constant.builtin.json"
---@field other string - Highlight group for other types, Default: "@label.json"
local parsers = require"jsonfly.parsers"
local utils = require"jsonfly.utils"
local cache = require"jsonfly.cache"
local insert = require"jsonfly.insert"
local parsers = require("jsonfly.parsers")
local utils = require("jsonfly.utils")
local cache = require("jsonfly.cache")
local insert = require("jsonfly.insert")
local json = require"jsonfly.json"
local finders = require "telescope.finders"
local pickers = require "telescope.pickers"
local json = require("jsonfly.json")
local finders = require("telescope.finders")
local pickers = require("telescope.pickers")
local conf = require("telescope.config").values
local make_entry = require "telescope.make_entry"
local entry_display = require "telescope.pickers.entry_display"
local make_entry = require("telescope.make_entry")
local entry_display = require("telescope.pickers.entry_display")
local action_state = require "telescope.actions.state"
local action_state = require("telescope.actions.state")
---@type Options
local DEFAULT_CONFIG = {
key_max_length = 50,
key_exact_length = false,
max_length = 9999,
overflow_marker = "",
conceal = "auto",
prompt_title = "JSON(fly)",
highlights = {
string = "@string.json",
number = "@number.json",
boolean = "@boolean.json",
null = "@constant.builtin.json",
other = "@label.json",
},
jump_behavior = "key_start",
subkeys_display = "normal",
show_nested_child_preview = true,
backend = "lsp",
use_cache = 500,
commands = {
add_key = {"i", "<C-a>"},
copy_jsonpath = {
"i",
"<C-j>",
---@param path string
---@param prompt_bufnr number
function(path, prompt_bufnr)
vim.fn.setreg("+", path)
end
}
}
key_max_length = 50,
key_exact_length = false,
max_length = 9999,
overflow_marker = "",
conceal = "auto",
prompt_title = "JSON(fly)",
highlights = {
string = "@string.json",
number = "@number.json",
boolean = "@boolean.json",
null = "@constant.builtin.json",
other = "@label.json",
},
jump_behavior = "key_start",
subkeys_display = "normal",
show_nested_child_preview = true,
backend = "lsp",
use_cache = 500,
commands = {
add_key = { "i", "<C-a>" },
copy_jsonpath = {
"i",
"<C-j>",
---@param path string
---@param prompt_bufnr number
function(path, prompt_bufnr)
vim.fn.setreg("+", path)
end,
},
},
}
local global_config = {}
@ -79,182 +79,207 @@ local global_config = {}
---@param entries Entry[]
---@param buffer number
local function show_picker(entries, buffer, xopts)
local config = vim.tbl_deep_extend("force", global_config, xopts or {})
local filename = vim.api.nvim_buf_get_name(buffer)
local config = vim.tbl_deep_extend("force", global_config, xopts or {})
local filename = vim.api.nvim_buf_get_name(buffer)
local displayer = entry_display.create {
separator = " ",
items = {
{ width = 1 },
global_config.key_exact_length and { width = global_config.key_max_length } or { remaining = true },
{ remaining = true },
},
}
---@type boolean
local conceal
local displayer = entry_display.create({
separator = " ",
items = {
{ width = 1 },
global_config.key_exact_length and { width = global_config.key_max_length } or { remaining = true },
{ remaining = true },
},
})
---@type boolean
local conceal
if global_config.conceal == "auto" then
conceal = vim.o.conceallevel > 0
else
conceal = global_config.conceal == true
end
if global_config.conceal == "auto" then
conceal = vim.o.conceallevel > 0
else
conceal = global_config.conceal == true
end
pickers.new(config, {
prompt_title = global_config.prompt_title,
attach_mappings = function(_, map)
map(
global_config.commands.add_key[1],
global_config.commands.add_key[2],
function(prompt_bufnr)
local current_picker = action_state.get_current_picker(prompt_bufnr)
local input = current_picker:_get_prompt()
pickers
.new(config, {
prompt_title = global_config.prompt_title,
attach_mappings = function(_, map)
map(global_config.commands.add_key[1], global_config.commands.add_key[2], function(prompt_bufnr)
local current_picker = action_state.get_current_picker(prompt_bufnr)
local input = current_picker:_get_prompt()
local key_descriptor = utils:extract_key_description(input)
local key_descriptor = utils:extract_key_description(input)
insert:insert_new_key(entries, key_descriptor, buffer)
end
)
insert:insert_new_key(entries, key_descriptor, buffer)
end)
if global_config.commands.copy_jsonpath and utils:is_module_available("jsonpath") then
map(
global_config.commands.copy_jsonpath[1],
global_config.commands.copy_jsonpath[2],
function(prompt_bufnr)
local jsonpath = require("jsonpath")
if global_config.commands.copy_jsonpath and utils:is_module_available("jsonpath") then
map(
global_config.commands.copy_jsonpath[1],
global_config.commands.copy_jsonpath[2],
function(prompt_bufnr)
local jsonpath = require("jsonpath")
local current_picker = action_state.get_current_picker(prompt_bufnr)
local selection = current_picker:get_selection()
local current_picker = action_state.get_current_picker(prompt_bufnr)
local selection = current_picker:get_selection()
local path = jsonpath.get(vim.treesitter.get_node({
bufnr = buffer,
pos = {
selection.lnum - 1,
selection.index,
}
}), buffer)
local path = jsonpath.get(
vim.treesitter.get_node({
bufnr = buffer,
pos = {
selection.lnum - 1,
selection.index,
},
}),
buffer
)
if path then
global_config.commands.copy_jsonpath[3](path, prompt_bufnr)
end
end
)
end
if path then
global_config.commands.copy_jsonpath[3](path, prompt_bufnr)
end
end
)
end
return true
end,
finder = finders.new_table {
results = entries,
---@param entry Entry
entry_maker = function(entry)
local _, raw_depth = entry.key:gsub("%.", ".")
local depth = (raw_depth or 0) + 1
return true
end,
finder = finders.new_table({
results = entries,
---@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, conceal, global_config.show_nested_child_preview)
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,
conceal,
global_config.show_nested_child_preview
)
local key = global_config.subkeys_display == "normal" and entry.key or utils:replace_previous_keys(entry.key, " ")
local key = global_config.subkeys_display == "normal" and entry.key
or utils:replace_previous_keys(entry.key, " ")
return displayer {
{ depth, "TelescopeResultsNumber"},
{
utils:truncate_overflow(
key,
global_config.key_max_length,
global_config.overflow_marker
),
"@property.json",
},
{
utils:truncate_overflow(
preview,
global_config.max_length,
global_config.overflow_marker
),
global_config.highlights[hl_group_key] or "TelescopeResultsString",
},
}
end,
return displayer({
{ depth, "TelescopeResultsNumber" },
{
utils:truncate_overflow(
key,
global_config.key_max_length,
global_config.overflow_marker
),
"@property.json",
},
{
utils:truncate_overflow(
preview,
global_config.max_length,
global_config.overflow_marker
),
global_config.highlights[hl_group_key] or "TelescopeResultsString",
},
})
end,
bufnr = buffer,
filename = filename,
lnum = entry.position.line_number,
col = global_config.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
}, config)
end,
},
previewer = conf.grep_previewer(config),
sorter = conf.generic_sorter(config),
sorting_strategy = "ascending",
}):find()
bufnr = buffer,
filename = filename,
lnum = entry.position.line_number,
col = global_config.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,
}, config)
end,
}),
previewer = conf.grep_previewer(config),
sorter = conf.generic_sorter(config),
sorting_strategy = "ascending",
})
:find()
end
return require("telescope").register_extension {
setup = function(extension_config)
global_config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, extension_config or {})
end,
exports = {
jsonfly = function(xopts)
local current_buf = vim.api.nvim_get_current_buf()
return require("telescope").register_extension({
setup = function(extension_config)
global_config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, extension_config or {})
end,
exports = {
jsonfly = function(xopts)
local current_buf = vim.api.nvim_get_current_buf()
local cached_entries = cache:get_cache(current_buf)
local cached_entries = cache:get_cache(current_buf)
if cached_entries ~= nil then
show_picker(cached_entries, current_buf, xopts)
return
end
if cached_entries ~= nil then
show_picker(cached_entries, current_buf, xopts)
return
end
local content_lines = vim.api.nvim_buf_get_lines(current_buf, 0, -1, false)
local content = table.concat(content_lines, "\n")
local allow_cache = global_config.use_cache > 0 and #content_lines >= global_config.use_cache
local content_lines = vim.api.nvim_buf_get_lines(current_buf, 0, -1, false)
local content = table.concat(content_lines, "\n")
local allow_cache = global_config.use_cache > 0 and #content_lines >= global_config.use_cache
if allow_cache then
cache:register_listeners(current_buf)
end
if allow_cache then
cache:register_listeners(current_buf)
end
local function run_lua_parser()
local parsed = json:decode(content)
local entries = parsers:get_entries_from_lua_json(parsed)
local function run_lua_parser()
local parsed = json:decode(content)
local entries = parsers:get_entries_from_lua_json(parsed)
if allow_cache then
cache:cache_buffer(current_buf, entries)
end
if allow_cache then
cache:cache_buffer(current_buf, entries)
end
show_picker(entries, current_buf, xopts)
end
show_picker(entries, current_buf, xopts)
end
if global_config.backend == "lsp" then
local params = vim.lsp.util.make_position_params(xopts.winnr)
if global_config.backend == "lsp" then
local params = vim.lsp.util.make_position_params(xopts.winnr)
vim.lsp.buf_request_all(
current_buf,
"textDocument/documentSymbol",
params,
function(response)
if response == nil or #response == 0 then
run_lua_parser()
return
end
-- Check i
local clients = vim.lsp.get_clients()
local result = response[1].result
local any_support = false
local entries = parsers:get_entries_from_lsp_symbols(result)
for _, client in ipairs(clients) do
if client.supports_method("textDocument/documentSymbol") then
any_support = true
break
end
end
if allow_cache then
cache:cache_buffer(current_buf, entries)
end
if any_support then
vim.lsp.buf_request_all(current_buf, "textDocument/documentSymbol", params, function(results)
if results == nil or vim.tbl_isempty(results) then
run_lua_parser()
return
end
show_picker(entries, current_buf, xopts)
end
)
else
run_lua_parser()
end
end
}
}
local combined_result = {}
for _, res in pairs(results) do
if res.result then
vim.list_extend(combined_result, res.result)
end
end
if vim.tbl_isempty(combined_result) then
run_lua_parser()
return
end
local entries = parsers:get_entries_from_lsp_symbols(combined_result)
if allow_cache then
cache:cache_buffer(current_buf, entries)
end
show_picker(entries, current_buf, xopts)
end)
end
end
run_lua_parser()
end,
},
})