From 50e65c966ad95a3b447b7d0162de2f4215fc813a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:14:09 +0200 Subject: [PATCH 01/23] wip: Adding command support --- lua/jsonfly/utils.lua | 61 +++++++++++++ lua/telescope/_extensions/jsonfly.lua | 126 ++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/lua/jsonfly/utils.lua b/lua/jsonfly/utils.lua index 66f30bf..5caa893 100644 --- a/lua/jsonfly/utils.lua +++ b/lua/jsonfly/utils.lua @@ -1,3 +1,7 @@ +---@class KeyDescription +---@field key string +---@field type "object"|"array"|"string" + local M = {} function M:truncate_overflow(value, max_length, overflow_marker) @@ -54,4 +58,61 @@ function M:replace_previous_keys(key, replacement) return key end +---@param text string +---@param char string +---@return string[] +function M:split_by_char(text, char) + local parts = {} + local current = "" + + 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 + + parts[#parts + 1] = current + + return parts +end + +---@param text string +---@return KeyDescription[] +function M:extract_key_description(text) + local keys = {} + + local splitted = M:split_by_char(text, ".") + for index=1, #splitted do + local token = splitted[index] + + if string.sub(token, 1, 1) == "[" then + keys[#keys + 1] = { + key = tonumber(string.sub(token, 2, -2)), + type = "array", + } + else + keys[#keys + 1] = { + key = token, + type = "object", + } + end + end + + if #keys == 0 then + return { + { + key = text, + type = "string", + } + } + end + + return keys +end + return M diff --git a/lua/telescope/_extensions/jsonfly.lua b/lua/telescope/_extensions/jsonfly.lua index cd0c19f..37ed070 100644 --- a/lua/telescope/_extensions/jsonfly.lua +++ b/lua/telescope/_extensions/jsonfly.lua @@ -31,6 +31,8 @@ local conf = require"telescope.config".values local make_entry = require "telescope.make_entry" local entry_display = require "telescope.pickers.entry_display" +local action_state = require "telescope.actions.state" + ---@type Options local opts = { key_max_length = 50, @@ -52,6 +54,118 @@ local opts = { use_cache = 500, } +-- https://stackoverflow.com/a/24823383/9878135 +function table.slice(tbl, first, last, step) + local sliced = {} + + for i = first or 1, last or #tbl, step or 1 do + sliced[#sliced+1] = tbl[i] + end + + return sliced +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, ".") + + return splitted[index] == key +end + +---@param entries Entry[] +---@param keys KeyDescription[] +---@return [Entry, string[]] +local function find_remaining_keys(entries, keys) + local start_index = 1 + local end_index = #keys + + local existing_keys_depth = nil + + for kk=1, #keys do + local found_result = false + local key = keys[kk].key + + for ii=start_index, #entries do + if check_key_equal(entries[ii], key, kk) then + found_result = true + start_index = ii + break + end + end + + for ii=start_index + 1, #entries do + if not check_key_equal(entries[ii], key, kk) then + found_result = true + end_index = ii - 1 + break + end + end + + if not found_result then + existing_keys_depth = kk - 1 + break + end + end + + if existing_keys_depth == nil then + existing_keys_depth = #keys + end + + local last_entry = entries[end_index] + local remaining_keys = table.slice(keys, existing_keys_depth, #keys) + + return { last_entry, remaining_keys } +end + +---@param keys KeyDescription +---@param index number - Index of the key +local function write_keys(keys, index) + local lines = {} + + if index >= #keys then + return {} + end + + lines[#lines + 1] = "," + lines[#lines + 1] = "\"" .. keys[index].key .. "\": {" + lines[#lines + 1] = write_keys(keys, index + 1) + lines[#lines + 1] = "}" + + return lines +end + +---@param entries Entry[] +---@param keys KeyDescription[] +---@param buffer number +local function insert_new_key(entries, keys, buffer) + local _result = find_remaining_keys(entries, keys) + local last_entry = _result[1] + local remaining_keys = _result[2] + + local writes = write_keys(remaining_keys, 1) + + print(vim.inspect(last_entry)) + print(vim.inspect(remaining_keys)) + print(vim.inspect(writes)) + + -- for parts=#keys, 1, -1 do + -- ---@type Entries[] + -- local sub_keys = table.slice(keys, 1, parts) + -- local path = "" + -- + -- for ii=1, #sub_keys do + -- path = path .. sub_keys[ii].key + -- if ii < #sub_keys then + -- path = path .. "." + -- end + -- end + -- + -- print(path) + -- end +end + ---@param entries Entry[] ---@param buffer number local function show_picker(entries, buffer) @@ -76,6 +190,18 @@ local function show_picker(entries, buffer) pickers.new(opts, { prompt_title = opts.prompt_title, + attach_mappings = function(_, map) + map("i", "", 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) + + insert_new_key(entries, key_descriptor, buffer) + end) + + return true + end, finder = finders.new_table { results = entries, ---@param entry Entry From eed1e273095cdfcb9f0966dcb9c4a9de1ac3a865 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:19:33 +0200 Subject: [PATCH 02/23] feat: Add first wip for inserting custom keys --- lua/telescope/_extensions/jsonfly.lua | 61 ++++++++++++++++++--------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/lua/telescope/_extensions/jsonfly.lua b/lua/telescope/_extensions/jsonfly.lua index 37ed070..f0c8c81 100644 --- a/lua/telescope/_extensions/jsonfly.lua +++ b/lua/telescope/_extensions/jsonfly.lua @@ -79,10 +79,11 @@ end ---@return [Entry, string[]] local function find_remaining_keys(entries, keys) local start_index = 1 - local end_index = #keys local existing_keys_depth = nil + local potential_keys_indexes = {} + for kk=1, #keys do local found_result = false local key = keys[kk].key @@ -91,20 +92,14 @@ local function find_remaining_keys(entries, keys) if check_key_equal(entries[ii], key, kk) then found_result = true start_index = ii - break - end - end - - for ii=start_index + 1, #entries do - if not check_key_equal(entries[ii], key, kk) then - found_result = true - end_index = ii - 1 - break + potential_keys_indexes[#potential_keys_indexes + 1] = ii + -- Not sure if this can be used for optimization + -- break end end if not found_result then - existing_keys_depth = kk - 1 + existing_keys_depth = kk break end end @@ -113,10 +108,29 @@ local function find_remaining_keys(entries, keys) existing_keys_depth = #keys end - local last_entry = entries[end_index] + local existing_keys = table.slice(keys, 1, existing_keys_depth - 1) local remaining_keys = table.slice(keys, existing_keys_depth, #keys) + local existing_key_str = "" + + for ii=1, #existing_keys do + existing_key_str = existing_key_str .. existing_keys[ii].key + if ii < #existing_keys then + existing_key_str = existing_key_str .. "." + end + end + + -- Find key that matches perfectly + for ii=1, #potential_keys_indexes do + local index = potential_keys_indexes[ii] + local entry = entries[index] - return { last_entry, remaining_keys } + if entry.key == existing_key_str then + return {entry, remaining_keys} + end + end + + -- Weird case, return the first one in hope that it's correct + return {potential_keys_indexes[1], remaining_keys} end ---@param keys KeyDescription @@ -124,13 +138,18 @@ end local function write_keys(keys, index) local lines = {} - if index >= #keys then - return {} + if index == #keys then + return { "\"" .. keys[index].key .. "\": \"\""} end - lines[#lines + 1] = "," + local insertions = write_keys(keys, index + 1) + lines[#lines + 1] = "\"" .. keys[index].key .. "\": {" - lines[#lines + 1] = write_keys(keys, index + 1) + + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + end + lines[#lines + 1] = "}" return lines @@ -140,13 +159,17 @@ end ---@param keys KeyDescription[] ---@param buffer number local function insert_new_key(entries, keys, buffer) + -- Close current buffer + vim.cmd [[quit!]] + local _result = find_remaining_keys(entries, keys) - local last_entry = _result[1] + local entry = _result[1] local remaining_keys = _result[2] local writes = write_keys(remaining_keys, 1) + vim.api.nvim_buf_set_lines(buffer, start_line, start_line, false, writes) - print(vim.inspect(last_entry)) + print(vim.inspect(entry)) print(vim.inspect(remaining_keys)) print(vim.inspect(writes)) From 98cdf4cceed826fd6e7ca1c3c74533773c1af5ef Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 26 Apr 2024 23:59:58 +0200 Subject: [PATCH 03/23] fix: Fix key insertion --- lua/jsonfly/insert.lua | 172 ++++++++++++++++++++++++++ lua/telescope/_extensions/jsonfly.lua | 138 +-------------------- 2 files changed, 174 insertions(+), 136 deletions(-) create mode 100644 lua/jsonfly/insert.lua diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua new file mode 100644 index 0000000..afc866d --- /dev/null +++ b/lua/jsonfly/insert.lua @@ -0,0 +1,172 @@ +local utils = require"jsonfly.utils" + + +local M = {}; + +-- https://stackoverflow.com/a/24823383/9878135 +function table.slice(tbl, first, last, step) + local sliced = {} + + for i = first or 1, last or #tbl, step or 1 do + sliced[#sliced+1] = tbl[i] + end + + return sliced +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, ".") + + return splitted[index] == key +end + +---Find the entry in `entries` with the most matching keys at the beginning based on the `keys`. +---Returns the index of the entry +---@param entries Entry[] +---@param keys KeyDescription[] +---@return number|nil +local function find_best_fitting_entry(entries, keys) + local entry_index + local current_indexes = {1, #entries} + + for kk=1, #keys do + local key = keys[kk].key + + local start_index = current_indexes[1] + local end_index = current_indexes[2] + + 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 + + current_indexes[2] = ii + end + end + + if current_indexes[1] == nil then + -- No entries found + break + else + entry_index = current_indexes[1] + end + end + + return entry_index +end + +---@param keys KeyDescription +---@param index number - Index of the key +local function write_keys(keys, index) + local lines = {} + + if index == #keys then + return { + { "\"" .. keys[index].key .. "\": \"\""}, + true + } + end + + local insertions = write_keys(keys, index + 1) + + lines[#lines + 1] = "\"" .. keys[index].key .. "\": {" + + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + end + + lines[#lines + 1] = "}" + + return lines +end + +---@param buffer number +---@param insertion_line number +local function add_comma(buffer, insertion_line) + 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, ii - BUFFER_SIZE, ii, false) + + if #previous_lines == 0 then + return + end + + for jj=#previous_lines, 0, -1 do + local line = previous_lines[jj] + + for char_index=#line, 0, -1 do + local char = line:sub(char_index, char_index) + + if char ~= " " and char ~= "\t" and char ~= "\n" and char ~= "\r" then + if char == "," then + return + end + + + -- Insert comma at position + vim.api.nvim_buf_set_text(buffer, ii - 1, char_index, ii - 1, char_index, {","}) + return + end + end + end + end +end + +---@param entries Entry[] +---@param keys KeyDescription[] +---@param buffer number +function M:insert_new_key(entries, keys, buffer) + -- Close current buffer + vim.cmd [[quit!]] + + local entry_index = find_best_fitting_entry(entries, keys) or 0 + local entry = entries[entry_index] + local existing_keys_depth = #utils:split_by_char(entry.key, ".") + 1 + local remaining_keys = table.slice(keys, existing_keys_depth, #keys) + + local _writes = write_keys(remaining_keys, 1) + 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 + + vim.api.nvim_win_set_cursor(0, {entry.position.line_number, entry.position.value_start}) + -- Hacky way to jump to end of object + vim.cmd [[execute "normal %"]] + local start_line = vim.api.nvim_win_get_cursor(0)[1] - 1 + + -- Add comma to previous line + add_comma(buffer, start_line) + -- + -- 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 =' .. #writes .. 'j"') + -- + -- -- Jump to the key + vim.api.nvim_win_set_cursor(0, {start_line + math.ceil(#writes / 2), 0}) + vim.cmd [[execute "normal $a"]] + + -- vim.schedule(function() + -- vim.cmd [[%]] + -- end) + +end + +return M; diff --git a/lua/telescope/_extensions/jsonfly.lua b/lua/telescope/_extensions/jsonfly.lua index f0c8c81..3c93d3d 100644 --- a/lua/telescope/_extensions/jsonfly.lua +++ b/lua/telescope/_extensions/jsonfly.lua @@ -23,6 +23,7 @@ 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" @@ -54,141 +55,6 @@ local opts = { use_cache = 500, } --- https://stackoverflow.com/a/24823383/9878135 -function table.slice(tbl, first, last, step) - local sliced = {} - - for i = first or 1, last or #tbl, step or 1 do - sliced[#sliced+1] = tbl[i] - end - - return sliced -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, ".") - - return splitted[index] == key -end - ----@param entries Entry[] ----@param keys KeyDescription[] ----@return [Entry, string[]] -local function find_remaining_keys(entries, keys) - local start_index = 1 - - local existing_keys_depth = nil - - local potential_keys_indexes = {} - - for kk=1, #keys do - local found_result = false - local key = keys[kk].key - - for ii=start_index, #entries do - if check_key_equal(entries[ii], key, kk) then - found_result = true - start_index = ii - potential_keys_indexes[#potential_keys_indexes + 1] = ii - -- Not sure if this can be used for optimization - -- break - end - end - - if not found_result then - existing_keys_depth = kk - break - end - end - - if existing_keys_depth == nil then - existing_keys_depth = #keys - end - - local existing_keys = table.slice(keys, 1, existing_keys_depth - 1) - local remaining_keys = table.slice(keys, existing_keys_depth, #keys) - local existing_key_str = "" - - for ii=1, #existing_keys do - existing_key_str = existing_key_str .. existing_keys[ii].key - if ii < #existing_keys then - existing_key_str = existing_key_str .. "." - end - end - - -- Find key that matches perfectly - for ii=1, #potential_keys_indexes do - local index = potential_keys_indexes[ii] - local entry = entries[index] - - if entry.key == existing_key_str then - return {entry, remaining_keys} - end - end - - -- Weird case, return the first one in hope that it's correct - return {potential_keys_indexes[1], remaining_keys} -end - ----@param keys KeyDescription ----@param index number - Index of the key -local function write_keys(keys, index) - local lines = {} - - if index == #keys then - return { "\"" .. keys[index].key .. "\": \"\""} - end - - local insertions = write_keys(keys, index + 1) - - lines[#lines + 1] = "\"" .. keys[index].key .. "\": {" - - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] - end - - lines[#lines + 1] = "}" - - return lines -end - ----@param entries Entry[] ----@param keys KeyDescription[] ----@param buffer number -local function insert_new_key(entries, keys, buffer) - -- Close current buffer - vim.cmd [[quit!]] - - local _result = find_remaining_keys(entries, keys) - local entry = _result[1] - local remaining_keys = _result[2] - - local writes = write_keys(remaining_keys, 1) - vim.api.nvim_buf_set_lines(buffer, start_line, start_line, false, writes) - - print(vim.inspect(entry)) - print(vim.inspect(remaining_keys)) - print(vim.inspect(writes)) - - -- for parts=#keys, 1, -1 do - -- ---@type Entries[] - -- local sub_keys = table.slice(keys, 1, parts) - -- local path = "" - -- - -- for ii=1, #sub_keys do - -- path = path .. sub_keys[ii].key - -- if ii < #sub_keys then - -- path = path .. "." - -- end - -- end - -- - -- print(path) - -- end -end - ---@param entries Entry[] ---@param buffer number local function show_picker(entries, buffer) @@ -220,7 +86,7 @@ local function show_picker(entries, buffer) local key_descriptor = utils:extract_key_description(input) - insert_new_key(entries, key_descriptor, buffer) + insert:insert_new_key(entries, key_descriptor, buffer) end) return true From d7e212580574b32b5d69448f83c082bd3cebc709 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 00:10:44 +0200 Subject: [PATCH 04/23] fix: Fix comma insertion --- lua/jsonfly/insert.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index afc866d..92604a1 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -99,10 +99,10 @@ local function add_comma(buffer, insertion_line) return end - for jj=#previous_lines, 0, -1 do + for jj=#previous_lines, 1, -1 do local line = previous_lines[jj] - for char_index=#line, 0, -1 do + 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 @@ -110,9 +110,16 @@ local function add_comma(buffer, insertion_line) return end - -- Insert comma at position - vim.api.nvim_buf_set_text(buffer, ii - 1, char_index, ii - 1, char_index, {","}) + local line_number = ii - (BUFFER_SIZE - jj) + vim.api.nvim_buf_set_text( + buffer, + line_number - 1, + char_index, + line_number - 1, + char_index, + {","} + ) return end end From fbc1aa0424c3b396ea7874b5bf8ff66b69e75786 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 00:16:01 +0200 Subject: [PATCH 05/23] fix: Improvements --- lua/jsonfly/insert.lua | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index 92604a1..f4a0996 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -151,29 +151,25 @@ function M:insert_new_key(entries, keys, buffer) end end - vim.api.nvim_win_set_cursor(0, {entry.position.line_number, entry.position.value_start}) -- 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 start_line = vim.api.nvim_win_get_cursor(0)[1] - 1 - -- Add comma to previous line + -- Add comma to previous JSON entry add_comma(buffer, start_line) - -- + -- Insert new lines vim.api.nvim_buf_set_lines(buffer, start_line, start_line, false, writes) - -- - -- -- -- Format lines + + -- Format lines vim.api.nvim_win_set_cursor(0, {start_line, 1}) vim.cmd('execute "normal =' .. #writes .. 'j"') - -- - -- -- Jump to the key - vim.api.nvim_win_set_cursor(0, {start_line + math.ceil(#writes / 2), 0}) - vim.cmd [[execute "normal $a"]] - - -- vim.schedule(function() - -- vim.cmd [[%]] - -- end) + -- Jump to the key + vim.api.nvim_win_set_cursor(0, {start_line + math.ceil(#writes / 2), 0}) + vim.cmd [[execute "normal $a"]] end return M; From b8ba2ec072f2de498eb07e7de78cc02d148ed862 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 00:20:03 +0200 Subject: [PATCH 06/23] feat: Add option for custom mappings --- lua/telescope/_extensions/jsonfly.lua | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lua/telescope/_extensions/jsonfly.lua b/lua/telescope/_extensions/jsonfly.lua index 3c93d3d..ed59e01 100644 --- a/lua/telescope/_extensions/jsonfly.lua +++ b/lua/telescope/_extensions/jsonfly.lua @@ -12,6 +12,10 @@ ---@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" ---@field use_cache number - Whether to use cache the parsed JSON. The cache will be activated if the number of lines is greater or equal to this value, By default, the cache is activate when the file if 1000 lines or more; `0` to disable the cache, Default: 500 +---@field commands Commands - Shortcuts for commands +-- +---@class Commands +---@field add_key string[] - Add the currently entered key to the JSON. Must be of type [string, string] ; Example: {"n", "a"} -> When in normal mode, press "a" to add the key; Example: {"i", ""} -> When in insert mode, press to add the key; Default: {"i", ""} --- ---@class Highlights ---@field number string - Highlight group for numbers, Default: "@number.json" @@ -53,6 +57,9 @@ local opts = { subkeys_display = "normal", backend = "lsp", use_cache = 500, + commands = { + add_key = {"i", ""} + } } ---@param entries Entry[] @@ -80,14 +87,18 @@ local function show_picker(entries, buffer) pickers.new(opts, { prompt_title = opts.prompt_title, attach_mappings = function(_, map) - map("i", "", function(prompt_bufnr) - local current_picker = action_state.get_current_picker(prompt_bufnr) - local input = current_picker:_get_prompt() + map( + opts.commands.add_key[1], + opts.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 + ) return true end, From 55bdb800fa8e3b1b95ddb33e9ed1d7676e5f650f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 16:53:57 +0200 Subject: [PATCH 07/23] fix: Properly write arrays; improve parser --- lua/jsonfly/insert.lua | 39 ++++++++++++++++++++---- lua/jsonfly/utils.lua | 67 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index f4a0996..330bc9e 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -74,14 +74,41 @@ local function write_keys(keys, index) end local insertions = write_keys(keys, index + 1) + local key = keys[index] - lines[#lines + 1] = "\"" .. keys[index].key .. "\": {" + if key.type == "object" then + lines[#lines + 1] = "\"" .. key.key .. "\": {" - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] - end + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + end - lines[#lines + 1] = "}" + lines[#lines + 1] = "}" + elseif key.type == "array" then + lines[#lines + 1] = "[" + + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + 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 key + lines[#lines + 1] = "{" + + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + end + + lines[#lines + 1] = "}" + end return lines end @@ -151,6 +178,8 @@ function M:insert_new_key(entries, keys, buffer) end end + print(vim.inspect(remaining_keys)) + -- 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 %"]] diff --git a/lua/jsonfly/utils.lua b/lua/jsonfly/utils.lua index 5caa893..bb3d953 100644 --- a/lua/jsonfly/utils.lua +++ b/lua/jsonfly/utils.lua @@ -1,6 +1,55 @@ ---@class KeyDescription ---@field key string ----@field type "object"|"array"|"string" +---@field type "object"|"array"|"array_index" + +-- Examples: +--{ +-- hello: [ +-- { +-- test: "abc" +-- } +-- ] +--} +-- hello.[0].test +-- { key = "hello", type = "object" } +-- { type = "array" } +-- { type = "array_index", key = 0 } +-- { key = "test", type = "object" } +-- +--{ +-- hello: [ +-- [ +-- { +-- test: "abc" +-- } +-- ] +-- ] +--} +-- hello.[0].[0].test +-- { key = "hello", type = "object" } +-- { type = "array" } +-- { type = "array_index", key = 0 } +-- { type = "array" } +-- { type = "array_index", key = 0 } +-- { key = "test", type = "object" } +-- +--{ +-- hello: [ +-- {}, +-- [ +-- { +-- test: "abc" +-- } +-- ] +-- ] +--} +-- hello.[1].[0].test +-- { key = "hello", type = "object" } +-- { type = "array" } +-- { type = "array_index", key = 1 } +-- { type = "array" } +-- { type = "array_index", key = 0 } +-- { key = "test", type = "object" } local M = {} @@ -87,27 +136,37 @@ function M:extract_key_description(text) local keys = {} local splitted = M:split_by_char(text, ".") - for index=1, #splitted do + + local index = 1 + + while index <= #splitted do local token = splitted[index] if string.sub(token, 1, 1) == "[" then + local array_index = tonumber(string.sub(token, 2, -2)) + keys[#keys + 1] = { - key = tonumber(string.sub(token, 2, -2)), type = "array", } + keys[#keys + 1] = { + key = array_index, + type = "array_index", + } else keys[#keys + 1] = { key = token, type = "object", } end + + index = index + 1 end if #keys == 0 then return { { key = text, - type = "string", + type = "object", } } end From ca9cbbda2482131162dcdec1a747ea68689c8844 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:05:40 +0200 Subject: [PATCH 08/23] fix: Improve general key descriptor and handler --- lua/jsonfly/insert.lua | 100 ++++++++++++++++++++++++++++++++--------- lua/jsonfly/utils.lua | 11 +++-- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index 330bc9e..60cf484 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -1,5 +1,9 @@ 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 = {}; @@ -26,14 +30,14 @@ end ---Find the entry in `entries` with the most matching keys at the beginning based on the `keys`. ---Returns the index of the entry ---@param entries Entry[] ----@param keys KeyDescription[] +---@param keys string[] ---@return number|nil local function find_best_fitting_entry(entries, keys) local entry_index local current_indexes = {1, #entries} for kk=1, #keys do - local key = keys[kk].key + local key = keys[kk] local start_index = current_indexes[1] local end_index = current_indexes[2] @@ -65,26 +69,38 @@ end ---@param index number - Index of the key local function write_keys(keys, index) local lines = {} + local key = keys[index] if index == #keys then return { - { "\"" .. keys[index].key .. "\": \"\""}, + { "\"" .. key.key .. "\": \"" .. CURSOR_SEARCH_HELPER .. "\""}, true } end local insertions = write_keys(keys, index + 1) - local key = keys[index] - if key.type == "object" then - lines[#lines + 1] = "\"" .. key.key .. "\": {" + if key.type == "object_wrapper" then + lines[#lines + 1] = "{" for ii=1, #insertions do lines[#lines + 1] = insertions[ii] end lines[#lines + 1] = "}" - elseif key.type == "array" then + elseif key.type == "key" then + lines[#lines + 1] = "\"" .. key.key .. "\":" + + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + end + elseif key.type == "array_key" then + lines[#lines + 1] = "\"" .. key.key .. "\":" + + for ii=1, #insertions do + lines[#lines + 1] = insertions[ii] + end + elseif key.type == "array_wrapper" then lines[#lines + 1] = "[" for ii=1, #insertions do @@ -94,20 +110,15 @@ local function write_keys(keys, index) 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 key - lines[#lines + 1] = "{" - for ii=1, #insertions do lines[#lines + 1] = insertions[ii] end - - lines[#lines + 1] = "}" end return lines @@ -154,17 +165,70 @@ local function add_comma(buffer, insertion_line) end end +---@param buffer number +function M:jump_to_cursor_helper(buffer) + 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, + {""} + ) + + -- -- Go into insert mode + vim.cmd [[execute "normal a"]] +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 + + for ii=1, #keys do + if keys[ii].type == "key" or keys[ii].type == "array_key" or keys[ii].type == "array_index" then + depth = depth + 1 + end + + if depth >= input_key_depth then + print(vim.inspect(ii)) + index = ii + break + end + end + + return index +end + ---@param entries Entry[] ---@param keys KeyDescription[] ---@param buffer number function M:insert_new_key(entries, keys, buffer) -- Close current buffer vim.cmd [[quit!]] + + local input_key = {} + + for ii=1, #keys do + if keys[ii].type == "key" then + input_key[#input_key+1] = keys[ii].key + elseif keys[ii].type == "array_index" then + input_key[#input_key+1] = keys[ii].key + end + end - local entry_index = find_best_fitting_entry(entries, keys) or 0 + print(vim.inspect(input_key)) + + local entry_index = find_best_fitting_entry(entries, input_key) or 0 local entry = entries[entry_index] - local existing_keys_depth = #utils:split_by_char(entry.key, ".") + 1 - local remaining_keys = table.slice(keys, existing_keys_depth, #keys) + local existing_input_keys_depth = #utils:split_by_char(entry.key, ".") + 1 + local existing_keys_index = get_key_descriptor_index(keys, existing_input_keys_depth) + local remaining_keys = table.slice(keys, existing_keys_index, #keys) local _writes = write_keys(remaining_keys, 1) local writes = {} @@ -178,8 +242,6 @@ function M:insert_new_key(entries, keys, buffer) end end - print(vim.inspect(remaining_keys)) - -- 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 %"]] @@ -196,9 +258,7 @@ function M:insert_new_key(entries, keys, buffer) vim.api.nvim_win_set_cursor(0, {start_line, 1}) vim.cmd('execute "normal =' .. #writes .. 'j"') - -- Jump to the key - vim.api.nvim_win_set_cursor(0, {start_line + math.ceil(#writes / 2), 0}) - vim.cmd [[execute "normal $a"]] + M:jump_to_cursor_helper(buffer) end return M; diff --git a/lua/jsonfly/utils.lua b/lua/jsonfly/utils.lua index bb3d953..f1880a0 100644 --- a/lua/jsonfly/utils.lua +++ b/lua/jsonfly/utils.lua @@ -1,6 +1,6 @@ ---@class KeyDescription ---@field key string ----@field type "object"|"array"|"array_index" +---@field type "object_wrapper"|"key"|"array_wrapper"|"array_index" -- Examples: --{ @@ -146,16 +146,19 @@ function M:extract_key_description(text) local array_index = tonumber(string.sub(token, 2, -2)) keys[#keys + 1] = { - type = "array", + type = "array_wrapper", } keys[#keys + 1] = { key = array_index, type = "array_index", } else + keys[#keys + 1] = { + type = "object_wrapper", + } keys[#keys + 1] = { key = token, - type = "object", + type = "key", } end @@ -166,7 +169,7 @@ function M:extract_key_description(text) return { { key = text, - type = "object", + type = "key", } } end From 48503b7a5055a343145982aa7e57ba8a076da263 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:19:55 +0200 Subject: [PATCH 09/23] fix: Improve key extractor; Allow more patterns --- lua/jsonfly/utils.lua | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lua/jsonfly/utils.lua b/lua/jsonfly/utils.lua index f1880a0..17c36b8 100644 --- a/lua/jsonfly/utils.lua +++ b/lua/jsonfly/utils.lua @@ -142,7 +142,19 @@ function M:extract_key_description(text) while index <= #splitted do local token = splitted[index] - if string.sub(token, 1, 1) == "[" then + -- 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] = { @@ -152,6 +164,18 @@ function M:extract_key_description(text) 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", From 2f751e585a1c9aa18ffe5d1d700dcd8e1998aa2a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 19:37:47 +0200 Subject: [PATCH 10/23] fix: Improve writing keys; general improvements --- lua/jsonfly/insert.lua | 109 +++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index 60cf484..36c82da 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -18,6 +18,17 @@ function table.slice(tbl, first, last, step) return sliced end +---@param line string +---@return boolean - Whether the line contains an empty JSON object +local function line_contains_empty_json(line) + -- 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 string.match(line, ".*.*[%]%}]%s*,?%s*") +end + ---@param entry Entry ---@param key string ---@param index number @@ -67,45 +78,39 @@ end ---@param keys KeyDescription ---@param index number - Index of the key -local function write_keys(keys, index) - local lines = {} +---@param lines string[] - Table to write the lines to +local function write_keys(keys, index, lines) local key = keys[index] if index == #keys then - return { - { "\"" .. key.key .. "\": \"" .. CURSOR_SEARCH_HELPER .. "\""}, - true - } + lines[#lines + 1] = "\"" .. key.key .. "\": \"" .. CURSOR_SEARCH_HELPER .. "\"" + return end - local insertions = write_keys(keys, index + 1) - if key.type == "object_wrapper" then - lines[#lines + 1] = "{" - - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] + local previous_line = lines[#lines] or "" + if line_contains_empty_json(previous_line) then + lines[#lines + 1] = "{" + else + lines[#lines] = previous_line .. " {" end + write_keys(keys, index + 1, lines) + lines[#lines + 1] = "}" elseif key.type == "key" then lines[#lines + 1] = "\"" .. key.key .. "\":" - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] - end - elseif key.type == "array_key" then - lines[#lines + 1] = "\"" .. key.key .. "\":" - - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] - end + write_keys(keys, index + 1, lines) elseif key.type == "array_wrapper" then - lines[#lines + 1] = "[" - - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] + local previous_line = lines[#lines] or "" + -- Starting and ending on same line + if line_contains_empty_json(previous_line) 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 @@ -115,13 +120,8 @@ local function write_keys(keys, index) lines[#lines + 1] = "{}," end - -- Write key - for ii=1, #insertions do - lines[#lines + 1] = insertions[ii] - end + write_keys(keys, index + 1, lines) end - - return lines end ---@param buffer number @@ -144,7 +144,7 @@ local function add_comma(buffer, insertion_line) local char = line:sub(char_index, char_index) if char ~= " " and char ~= "\t" and char ~= "\n" and char ~= "\r" then - if char == "," then + if char == "," or char == "{" or char == "[" then return end @@ -165,6 +165,28 @@ local function add_comma(buffer, insertion_line) 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 "" + + if line_contains_empty_json(line) then + vim.api.nvim_buf_set_lines( + buffer, + line_number, + line_number + 1, + false, + { + "{", + "}," + } + ) + + return line_number + 1 + end + + return line_number +end + ---@param buffer number function M:jump_to_cursor_helper(buffer) vim.fn.search(CURSOR_SEARCH_HELPER) @@ -191,12 +213,11 @@ local function get_key_descriptor_index(keys, input_key_depth) local index = 0 for ii=1, #keys do - if keys[ii].type == "key" or keys[ii].type == "array_key" or keys[ii].type == "array_index" then + if keys[ii].type == "key" or keys[ii].type == "array_index" then depth = depth + 1 end if depth >= input_key_depth then - print(vim.inspect(ii)) index = ii break end @@ -211,26 +232,25 @@ end function M:insert_new_key(entries, keys, buffer) -- Close current buffer vim.cmd [[quit!]] - + local input_key = {} for ii=1, #keys do if keys[ii].type == "key" then - input_key[#input_key+1] = keys[ii].key + input_key[#input_key + 1] = keys[ii].key elseif keys[ii].type == "array_index" then - input_key[#input_key+1] = keys[ii].key + input_key[#input_key + 1] = tostring(keys[ii].key) end end - print(vim.inspect(input_key)) - local entry_index = find_best_fitting_entry(entries, input_key) or 0 local entry = entries[entry_index] local existing_input_keys_depth = #utils:split_by_char(entry.key, ".") + 1 local existing_keys_index = get_key_descriptor_index(keys, existing_input_keys_depth) local remaining_keys = table.slice(keys, existing_keys_index, #keys) - local _writes = write_keys(remaining_keys, 1) + local _writes = {} + write_keys(remaining_keys, 1, _writes) local writes = {} for ii=1, #_writes do @@ -246,17 +266,24 @@ function M:insert_new_key(entries, keys, buffer) 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 -- Add comma to previous JSON entry add_comma(buffer, start_line) + 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 -- 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 =' .. #writes .. 'j"') + vim.cmd('execute "normal =' .. changes .. 'j"') M:jump_to_cursor_helper(buffer) end From e427c5f2248af327aab7a0c9300696fb0656ce45 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:34:37 +0200 Subject: [PATCH 11/23] feat: Add luaunit --- luaunit | 1 + tests/tests.lua | 5 +++++ 2 files changed, 6 insertions(+) create mode 160000 luaunit create mode 100644 tests/tests.lua diff --git a/luaunit b/luaunit new file mode 160000 index 0000000..0e0d3dd --- /dev/null +++ b/luaunit @@ -0,0 +1 @@ +Subproject commit 0e0d3dd06fe1955a01f0e6763bc8dc6847ee3e8d diff --git a/tests/tests.lua b/tests/tests.lua new file mode 100644 index 0000000..fb7f278 --- /dev/null +++ b/tests/tests.lua @@ -0,0 +1,5 @@ +local lu = require"luaunit.luaunit" + + +os.exit( lu.LuaUnit.run() ) + From 342823df2093fbdfcbafb1852d33db0d0968d815 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:46:09 +0200 Subject: [PATCH 12/23] test: Add tests for utils module --- tests/test_utils.lua | 198 +++++++++++++++++++++++++++++++++++++++++++ tests/tests.lua | 5 -- 2 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 tests/test_utils.lua delete mode 100644 tests/tests.lua diff --git a/tests/test_utils.lua b/tests/test_utils.lua new file mode 100644 index 0000000..e934dbb --- /dev/null +++ b/tests/test_utils.lua @@ -0,0 +1,198 @@ +local lu = require"luaunit.luaunit" +local utils = require"lua.jsonfly.utils" + +function testBasicKey() + local key = "foo.bar" + ---@type KeyDescription[] + local EXPECTED = { + { + type = "object_wrapper", + }, + { + type = "key", + key = "foo", + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "bar", + } + } + + local descriptor = utils:extract_key_description(key) + + lu.assertEquals(descriptor, EXPECTED) +end + +function testArrayKey() + local key = "foo.0.bar" + ---@type KeyDescription[] + local EXPECTED = { + { + type = "object_wrapper", + }, + { + type = "key", + key = "foo", + }, + { + type = "array_wrapper", + }, + { + type = "array_index", + key = 0, + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "bar", + } + } + + local descriptor = utils:extract_key_description(key) + + lu.assertEquals(descriptor, EXPECTED) +end + +function testNestedArrayKey() + local key = "foo.0.bar.1.baz" + ---@type KeyDescription[] + local EXPECTED = { + { + type = "object_wrapper", + }, + { + type = "key", + key = "foo", + }, + { + type = "array_wrapper", + }, + { + type = "array_index", + key = 0, + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "bar", + }, + { + type = "array_wrapper", + }, + { + type = "array_index", + key = 1, + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "baz", + } + } + + local descriptor = utils:extract_key_description(key) + + lu.assertEquals(descriptor, EXPECTED) +end + +function testEscapedArrayDoesNotCreateArray() + local key = "foo.\\0.bar" + ---@type KeyDescription[] + local EXPECTED = { + { + type = "object_wrapper", + }, + { + type = "key", + key = "foo", + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "0", + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "bar", + } + } + + local descriptor = utils:extract_key_description(key) + + lu.assertEquals(descriptor, EXPECTED) +end + +function testBracketArrayKey() + local key = "foo.[0].bar" + ---@type KeyDescription[] + local EXPECTED = { + { + type = "object_wrapper", + }, + { + type = "key", + key = "foo", + }, + { + type = "array_wrapper", + }, + { + type = "array_index", + key = 0, + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "bar", + } + } + + local descriptor = utils:extract_key_description(key) + + lu.assertEquals(descriptor, EXPECTED) +end + +function testRootArrayKey() + local key = "0.foo" + ---@type KeyDescription[] + local EXPECTED = { + { + type = "array_wrapper", + }, + { + type = "array_index", + key = 0, + }, + { + type = "object_wrapper", + }, + { + type = "key", + key = "foo", + } + } + + local descriptor = utils:extract_key_description(key) + + lu.assertEquals(descriptor, EXPECTED) +end + + +os.exit( lu.LuaUnit.run() ) + diff --git a/tests/tests.lua b/tests/tests.lua deleted file mode 100644 index fb7f278..0000000 --- a/tests/tests.lua +++ /dev/null @@ -1,5 +0,0 @@ -local lu = require"luaunit.luaunit" - - -os.exit( lu.LuaUnit.run() ) - From 9d581c501709f33be772138f79861c93f6d8ebc8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:51:20 +0200 Subject: [PATCH 13/23] feat(ci-cd): Run lua tests on pull requests --- .github/workflows/run-tests.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/run-tests.yaml diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml new file mode 100644 index 0000000..9fcf87c --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -0,0 +1,19 @@ +name: Run tests + +on: + pull_request: + +jobs: + debug-builds: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Lua + uses: leafo/gh-actions-lua@v10 + with: + luaVersion: "5.4.6" + + - name: Run tests + run: lua -v ./tests/$(ls ./tests) + From e0323d2e13d8ef02d91612d17796c0e8e7dd5dfa Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:54:04 +0200 Subject: [PATCH 14/23] fix(ci-cd): Recursively checkout submodules --- .github/workflows/run-tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 9fcf87c..8e18e29 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -8,6 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: "recursive" - name: Install Lua uses: leafo/gh-actions-lua@v10 From 9435fbe1f384c63c537a0063c64929341265b568 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:58:05 +0200 Subject: [PATCH 15/23] fix: Add luaunit as submodule --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4b72ba1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "luaunit"] + path = luaunit + url = https://github.com/bluebird75/luaunit From a287e403ea814068739813a39e9e67868d40d630 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:06:03 +0200 Subject: [PATCH 16/23] fix: Remove wrong bracket expansion --- lua/jsonfly/insert.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index 36c82da..a707d68 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -19,14 +19,15 @@ function table.slice(tbl, first, last, step) 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) +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 string.match(line, ".*.*[%]%}]%s*,?%s*") + or (also_match_end_bracket and string.match(line, ".*.*[%]%}]%s*,?%s*")) end ---@param entry Entry @@ -87,9 +88,9 @@ local function write_keys(keys, index, lines) return end - if key.type == "object_wrapper" then + if key.type == "object_wrapper" and #lines > 0 then local previous_line = lines[#lines] or "" - if line_contains_empty_json(previous_line) then + if line_contains_empty_json(previous_line, true) then lines[#lines + 1] = "{" else lines[#lines] = previous_line .. " {" @@ -105,7 +106,7 @@ local function write_keys(keys, index, 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) then + if line_contains_empty_json(previous_line, true) then lines[#lines + 1] = "[" else lines[#lines] = previous_line .. " [" @@ -169,7 +170,7 @@ end 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 "" - if line_contains_empty_json(line) then + if line_contains_empty_json(line, false) then vim.api.nvim_buf_set_lines( buffer, line_number, From 85c097b59c0321ab4f451dbb401ec8463a4bd981 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:26:46 +0200 Subject: [PATCH 17/23] fix: Correctly determine required remaining keys if some already exist --- lua/jsonfly/insert.lua | 68 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index a707d68..e2f23c5 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -228,28 +228,80 @@ local function get_key_descriptor_index(keys, input_key_depth) end ---@param entries Entry[] ----@param keys KeyDescription[] ----@param buffer number -function M:insert_new_key(entries, keys, buffer) - -- Close current buffer - vim.cmd [[quit!]] +---@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, ".") + + local found = true + + for jj=1, #keys do + if keys[jj] ~= splitted[jj] then + found = false + break + end + end - local input_key = {} + if found then + return ii + end + end +end + +---@param keys KeyDescription[] +---@return string[] +local function flat_key_description(keys) + local flat_keys = {} for ii=1, #keys do if keys[ii].type == "key" then - input_key[#input_key + 1] = keys[ii].key + flat_keys[#flat_keys + 1] = keys[ii].key elseif keys[ii].type == "array_index" then - input_key[#input_key + 1] = tostring(keys[ii].key) + flat_keys[#flat_keys + 1] = tostring(keys[ii].key) end end + return flat_keys +end + +---Subtracts indexes if there are other indexes before already +---This ensures that no extra objects are created in `write_keys` +---Example: Entry got 4 indexes, keys want to index `6`. This will subtract 4 from `6` to get `2`. +---@param entries Entry[] +---@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 = flat_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 +end + +---@param entries Entry[] +---@param keys KeyDescription[] +---@param buffer number +function M:insert_new_key(entries, keys, buffer) + -- Close current buffer + vim.cmd [[quit!]] + + local input_key = flat_key_description(keys) local entry_index = find_best_fitting_entry(entries, input_key) or 0 local entry = entries[entry_index] local existing_input_keys_depth = #utils:split_by_char(entry.key, ".") + 1 local existing_keys_index = get_key_descriptor_index(keys, existing_input_keys_depth) local 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 + + -- normalize_array_indexes(remaining_keys) + local _writes = {} write_keys(remaining_keys, 1, _writes) local writes = {} From 49d5a258bc6bdcd2f13ab2910e15ff78fe997958 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:42:41 +0200 Subject: [PATCH 18/23] fix: Fix starting bracket missing on key insertion --- lua/jsonfly/insert.lua | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index e2f23c5..e308528 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -23,11 +23,11 @@ end ---@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*") + 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*")) + or (also_match_end_bracket and string.match(line, ".*.*[%}%]]%s*,?%s*")) end ---@param entry Entry @@ -88,9 +88,9 @@ local function write_keys(keys, index, lines) return end - if key.type == "object_wrapper" and #lines > 0 then + if key.type == "object_wrapper" then local previous_line = lines[#lines] or "" - if line_contains_empty_json(previous_line, true) then + if line_contains_empty_json(previous_line, true) or #lines == 0 then lines[#lines + 1] = "{" else lines[#lines] = previous_line .. " {" @@ -106,7 +106,7 @@ local function write_keys(keys, index, 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) then + if line_contains_empty_json(previous_line, true) or #lines == 0 then lines[#lines + 1] = "[" else lines[#lines] = previous_line .. " [" @@ -300,8 +300,6 @@ function M:insert_new_key(entries, keys, buffer) normalize_array_indexes(entries, starting_keys, remaining_keys[1]) end - -- normalize_array_indexes(remaining_keys) - local _writes = {} write_keys(remaining_keys, 1, _writes) local writes = {} From 66611884e0d3f75616316f478a813f11363a7d14 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 28 Apr 2024 21:57:01 +0200 Subject: [PATCH 19/23] fix: Properly handle top level arrays --- lua/jsonfly/insert.lua | 51 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index e308528..5f700c9 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -280,6 +280,18 @@ local function normalize_array_indexes(entries, starting_keys, key) 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 + + return #entries +end + ---@param entries Entry[] ---@param keys KeyDescription[] ---@param buffer number @@ -289,15 +301,42 @@ function M:insert_new_key(entries, keys, buffer) local input_key = flat_key_description(keys) local entry_index = find_best_fitting_entry(entries, input_key) or 0 + ---@type Entry local entry = entries[entry_index] - local existing_input_keys_depth = #utils:split_by_char(entry.key, ".") + 1 - local existing_keys_index = get_key_descriptor_index(keys, existing_input_keys_depth) - local 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) + ---@type KeyDescription[] + local remaining_keys + ---@type integer + local existing_keys_index + + 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 - normalize_array_indexes(entries, starting_keys, remaining_keys[1]) + 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 local _writes = {} From b7ac8e81ed3f9f49ce0d697ea87cc0913445eb48 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 1 May 2024 22:09:57 +0200 Subject: [PATCH 20/23] docs: Add info about inserting keys --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 6c661f8..4c6bcfa 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ It's completely customizable and even supports highlighting of the values. ## Features * 🔍 Search for deeply nested keys - `expo.android.imageAsset.0.uri` +* ⏎ Insert keys quickly into your buffer * 🎨 See values with their correct syntax highlighting (numbers, strings, booleans, null; configurable) * 💻 Use your LSP or the built-in JSON parser * 🗑 Values automatically cached for faster navigation @@ -67,6 +68,21 @@ Go to a JSON file and run: ```lua :Telescope jsonfly + +Now you can search for keys, subkeys, part of keys etc. + +### Inserting Keys + +If you search for a key that doesn't exist you can add it to your buffer by pressing `` (CTRL + a). + +You can enter nested keys, arrays, indices, subkeys etc. JSON(fly) will automatically manage everything for you. + +The following schemas are valid: + +* Nested keys: `expo.android.imageAssets.` +* Array indices: `expo.android.imageAssets.0.uri`, `expo.android.imageAssets.3.uri`, `expo.android.imageAssets.[3].uri` +* Escaping: `expo.android.tests.\0.name` -> Will not create an array but a key with the name `0` + ## See also * [jsonpath.nvim](https://github.com/phelipetls/jsonpath.nvim) - Copy JSON paths to your clipboard From 1c2fe2b24ce38c02c11af6de4bb59bfdbcf2bd56 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 1 May 2024 22:34:58 +0200 Subject: [PATCH 21/23] docs: Add minified json hint --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4c6bcfa..d4aa0b1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ The following schemas are valid: * Array indices: `expo.android.imageAssets.0.uri`, `expo.android.imageAssets.3.uri`, `expo.android.imageAssets.[3].uri` * Escaping: `expo.android.tests.\0.name` -> Will not create an array but a key with the name `0` +Please note: JSON(fly) is intended to be used with **human-readable** JSON files. Inserting keys won't work with minified JSON files. + ## See also * [jsonpath.nvim](https://github.com/phelipetls/jsonpath.nvim) - Copy JSON paths to your clipboard From 65cdbb3283e829f7322aad83882efab2759d82fd Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 1 May 2024 22:35:26 +0200 Subject: [PATCH 22/23] fix: Improve edge case where file lines is shorter than BUFFER_SIZE --- lua/jsonfly/insert.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index 5f700c9..11e8648 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -132,7 +132,14 @@ local function add_comma(buffer, insertion_line) -- 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, ii - BUFFER_SIZE, ii, false) + local previous_lines = vim.api.nvim_buf_get_lines( + buffer, + math.max(0, ii - BUFFER_SIZE), + ii, + false + ) + + print("previous lins: " .. vim.inspect(previous_lines)) if #previous_lines == 0 then return @@ -150,12 +157,12 @@ local function add_comma(buffer, insertion_line) end -- Insert comma at position - local line_number = ii - (BUFFER_SIZE - jj) + local line_number = math.max(0, ii - BUFFER_SIZE) + jj - 1 vim.api.nvim_buf_set_text( buffer, - line_number - 1, + line_number, char_index, - line_number - 1, + line_number, char_index, {","} ) From 47775bb746049ca13342de47f659e949ea6876f1 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 3 May 2024 19:58:15 +0200 Subject: [PATCH 23/23] fix: Small improvements --- README.md | 4 +- lua/jsonfly/insert.lua | 116 +++++++++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index d4aa0b1..f4bdef6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It's completely customizable and even supports highlighting of the values. ## Features * 🔍 Search for deeply nested keys - `expo.android.imageAsset.0.uri` -* ⏎ Insert keys quickly into your buffer +* 👀 Insert nested keys quickly into your buffer * 🎨 See values with their correct syntax highlighting (numbers, strings, booleans, null; configurable) * 💻 Use your LSP or the built-in JSON parser * 🗑 Values automatically cached for faster navigation @@ -73,6 +73,8 @@ Now you can search for keys, subkeys, part of keys etc. ### Inserting Keys +JSON(fly) supports inserting your current search prompt into your buffer. + If you search for a key that doesn't exist you can add it to your buffer by pressing `` (CTRL + a). You can enter nested keys, arrays, indices, subkeys etc. JSON(fly) will automatically manage everything for you. diff --git a/lua/jsonfly/insert.lua b/lua/jsonfly/insert.lua index 11e8648..9f3418f 100644 --- a/lua/jsonfly/insert.lua +++ b/lua/jsonfly/insert.lua @@ -139,8 +139,6 @@ local function add_comma(buffer, insertion_line) false ) - print("previous lins: " .. vim.inspect(previous_lines)) - if #previous_lines == 0 then return end @@ -178,6 +176,9 @@ 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 "" 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, @@ -185,7 +186,7 @@ local function expand_empty_object(buffer, line_number) false, { "{", - "}," + "}" .. remaining_line } ) @@ -195,25 +196,6 @@ local function expand_empty_object(buffer, line_number) return line_number end ----@param buffer number -function M:jump_to_cursor_helper(buffer) - 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, - {""} - ) - - -- -- Go into insert mode - vim.cmd [[execute "normal a"]] -end - ---@param keys KeyDescription[] ---@param input_key_depth number local function get_key_descriptor_index(keys, input_key_depth) @@ -259,7 +241,7 @@ end ---@param keys KeyDescription[] ---@return string[] -local function flat_key_description(keys) +local function flatten_key_description(keys) local flat_keys = {} for ii=1, #keys do @@ -280,7 +262,7 @@ 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 = flat_key_description(starting_keys) + 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] @@ -299,6 +281,27 @@ local function count_array_children(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) + + -- 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"]] +end + +-- TODO: Handle top level empty arrays ---@param entries Entry[] ---@param keys KeyDescription[] ---@param buffer number @@ -306,27 +309,18 @@ function M:insert_new_key(entries, keys, buffer) -- Close current buffer vim.cmd [[quit!]] - local input_key = flat_key_description(keys) - local entry_index = find_best_fitting_entry(entries, input_key) or 0 - ---@type Entry - local entry = entries[entry_index] + local input_key = flatten_key_description(keys) + ---@type boolean + local should_add_comma = true ---@type KeyDescription[] local remaining_keys - ---@type integer - local existing_keys_index + ---@type Entry + local entry - if entry == nil then - -- Insert as root - existing_keys_index = 0 + if #entries == 0 then 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 - entry = { key = "", position = { @@ -335,14 +329,42 @@ function M:insert_new_key(entries, keys, buffer) value_start = 1 } } + should_add_comma = false 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) + local entry_index = find_best_fitting_entry(entries, input_key) or 0 + entry = entries[entry_index] - 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]) + ---@type integer + local existing_keys_index + + 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 + + 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 @@ -367,7 +389,9 @@ function M:insert_new_key(entries, keys, buffer) local start_line = vim.api.nvim_win_get_cursor(0)[1] - 1 -- Add comma to previous JSON entry - add_comma(buffer, start_line) + 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