mirror of
https://github.com/neovim/neovim.git
synced 2025-09-28 14:08:32 +00:00
feat(json): pretty-format (indent) with vim.json.encode() #35424
Problem: There is no straightforward way to pretty-print objects as JSON. The existing `vim.inspect` outputs LON. Solution: Introduce an `indent` option for `vim.json.encode()` which enables human-readable output with configurable indentation. Adapts PR to upstream: openresty/lua-cjson#114
This commit is contained in:
@@ -3372,6 +3372,9 @@ vim.json.encode({obj}, {opts}) *vim.json.encode()*
|
|||||||
• {opts} (`table<string,any>?`) Options table with keys:
|
• {opts} (`table<string,any>?`) Options table with keys:
|
||||||
• escape_slash: (boolean) (default false) Escape slash
|
• escape_slash: (boolean) (default false) Escape slash
|
||||||
characters "/" in string values.
|
characters "/" in string values.
|
||||||
|
• indent: (string) (default "") String used for indentation at
|
||||||
|
each nesting level. If non-empty enables newlines and a
|
||||||
|
space after colons.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`string`)
|
(`string`)
|
||||||
|
@@ -262,6 +262,7 @@ LUA
|
|||||||
• |vim.list.unique()| to deduplicate lists.
|
• |vim.list.unique()| to deduplicate lists.
|
||||||
• |vim.list.bisect()| for binary search.
|
• |vim.list.bisect()| for binary search.
|
||||||
• Experimental `vim.pos` and `vim.range` for Position/Range abstraction.
|
• Experimental `vim.pos` and `vim.range` for Position/Range abstraction.
|
||||||
|
• |vim.json.encode()| has an `indent` option for pretty-formatting.
|
||||||
|
|
||||||
OPTIONS
|
OPTIONS
|
||||||
|
|
||||||
|
@@ -38,5 +38,7 @@ function vim.json.decode(str, opts) end
|
|||||||
---@param opts? table<string,any> Options table with keys:
|
---@param opts? table<string,any> Options table with keys:
|
||||||
--- - escape_slash: (boolean) (default false) Escape slash
|
--- - escape_slash: (boolean) (default false) Escape slash
|
||||||
--- characters "/" in string values.
|
--- characters "/" in string values.
|
||||||
|
--- - indent: (string) (default "") String used for indentation at each nesting level.
|
||||||
|
--- If non-empty enables newlines and a space after colons.
|
||||||
---@return string
|
---@return string
|
||||||
function vim.json.encode(obj, opts) end
|
function vim.json.encode(obj, opts) end
|
||||||
|
87
src/cjson/lua_cjson.c
vendored
87
src/cjson/lua_cjson.c
vendored
@@ -87,6 +87,7 @@
|
|||||||
#define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0
|
#define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0
|
||||||
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
|
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
|
||||||
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
|
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
|
||||||
|
#define DEFAULT_ENCODE_INDENT NULL
|
||||||
|
|
||||||
#ifdef DISABLE_INVALID_NUMBERS
|
#ifdef DISABLE_INVALID_NUMBERS
|
||||||
#undef DEFAULT_DECODE_INVALID_NUMBERS
|
#undef DEFAULT_DECODE_INVALID_NUMBERS
|
||||||
@@ -168,6 +169,7 @@ typedef struct {
|
|||||||
int encode_keep_buffer;
|
int encode_keep_buffer;
|
||||||
int encode_empty_table_as_object;
|
int encode_empty_table_as_object;
|
||||||
int encode_escape_forward_slash;
|
int encode_escape_forward_slash;
|
||||||
|
const char *encode_indent;
|
||||||
|
|
||||||
int decode_invalid_numbers;
|
int decode_invalid_numbers;
|
||||||
int decode_max_depth;
|
int decode_max_depth;
|
||||||
@@ -177,6 +179,7 @@ typedef struct {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char **char2escape[256];
|
const char **char2escape[256];
|
||||||
|
const char *indent;
|
||||||
} json_encode_options_t;
|
} json_encode_options_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -330,6 +333,20 @@ static int json_enum_option(lua_State *l, int optindex, int *setting,
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Process string option for a configuration function */
|
||||||
|
/*
|
||||||
|
static int json_string_option(lua_State *l, int optindex, const char **setting)
|
||||||
|
{
|
||||||
|
if (!lua_isnil(l, optindex)) {
|
||||||
|
const char *value = luaL_checkstring(l, optindex);
|
||||||
|
*setting = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pushstring(l, *setting ? *setting : "");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/* Configures handling of extremely sparse arrays:
|
/* Configures handling of extremely sparse arrays:
|
||||||
* convert: Convert extremely sparse arrays into objects? Otherwise error.
|
* convert: Convert extremely sparse arrays into objects? Otherwise error.
|
||||||
* ratio: 0: always allow sparse; 1: never allow sparse; >1: use ratio
|
* ratio: 0: always allow sparse; 1: never allow sparse; >1: use ratio
|
||||||
@@ -436,6 +453,20 @@ static int json_cfg_encode_keep_buffer(lua_State *l)
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Configure how to indent output */
|
||||||
|
/*
|
||||||
|
static int json_cfg_encode_indent(lua_State *l)
|
||||||
|
{
|
||||||
|
json_config_t *cfg = json_arg_init(l, 1);
|
||||||
|
|
||||||
|
json_string_option(l, 1, &cfg->encode_indent);
|
||||||
|
// simplify further checking
|
||||||
|
if (cfg->encode_indent[0] == '\0') cfg->encode_indent = NULL;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#if defined(DISABLE_INVALID_NUMBERS) && !defined(USE_INTERNAL_FPCONV)
|
#if defined(DISABLE_INVALID_NUMBERS) && !defined(USE_INTERNAL_FPCONV)
|
||||||
void json_verify_invalid_number_setting(lua_State *l, int *setting)
|
void json_verify_invalid_number_setting(lua_State *l, int *setting)
|
||||||
{
|
{
|
||||||
@@ -533,6 +564,7 @@ static void json_create_config(lua_State *l)
|
|||||||
cfg->decode_array_with_array_mt = DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT;
|
cfg->decode_array_with_array_mt = DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT;
|
||||||
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
|
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
|
||||||
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
|
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
|
||||||
|
cfg->encode_indent = DEFAULT_ENCODE_INDENT;
|
||||||
|
|
||||||
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
|
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
|
||||||
strbuf_init(&cfg->encode_buf, 0);
|
strbuf_init(&cfg->encode_buf, 0);
|
||||||
@@ -704,6 +736,13 @@ static void json_check_encode_depth(lua_State *l, json_config_t *cfg,
|
|||||||
static int json_append_data(lua_State *l, json_encode_t *cfg,
|
static int json_append_data(lua_State *l, json_encode_t *cfg,
|
||||||
int current_depth);
|
int current_depth);
|
||||||
|
|
||||||
|
static void json_append_newline_and_indent(strbuf_t *json, json_encode_t *ctx, int depth)
|
||||||
|
{
|
||||||
|
strbuf_append_char(json, '\n');
|
||||||
|
for (int i = 0; i < depth; i++)
|
||||||
|
strbuf_append_string(json, ctx->options->indent);
|
||||||
|
}
|
||||||
|
|
||||||
/* json_append_array args:
|
/* json_append_array args:
|
||||||
* - lua_State
|
* - lua_State
|
||||||
* - JSON strbuf
|
* - JSON strbuf
|
||||||
@@ -712,15 +751,22 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept
|
|||||||
int array_length, int raw)
|
int array_length, int raw)
|
||||||
{
|
{
|
||||||
int comma, i, json_pos, err;
|
int comma, i, json_pos, err;
|
||||||
|
int has_items = 0;
|
||||||
strbuf_t *json = ctx->json;
|
strbuf_t *json = ctx->json;
|
||||||
|
|
||||||
strbuf_append_char(json, '[');
|
strbuf_append_char(json, '[');
|
||||||
|
|
||||||
comma = 0;
|
comma = 0;
|
||||||
for (i = 1; i <= array_length; i++) {
|
for (i = 1; i <= array_length; i++) {
|
||||||
|
has_items = 1;
|
||||||
|
|
||||||
json_pos = strbuf_length(json);
|
json_pos = strbuf_length(json);
|
||||||
if (comma++ > 0)
|
if (comma++ > 0)
|
||||||
strbuf_append_char(json, ',');
|
strbuf_append_char(json, ',');
|
||||||
|
|
||||||
|
if (ctx->options->indent)
|
||||||
|
json_append_newline_and_indent(json, ctx, current_depth);
|
||||||
|
|
||||||
if (raw) {
|
if (raw) {
|
||||||
lua_rawgeti(l, -1, i);
|
lua_rawgeti(l, -1, i);
|
||||||
} else {
|
} else {
|
||||||
@@ -742,6 +788,9 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept
|
|||||||
lua_pop(l, 1);
|
lua_pop(l, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (has_items && ctx->options->indent)
|
||||||
|
json_append_newline_and_indent(json, ctx, current_depth-1);
|
||||||
|
|
||||||
strbuf_append_char(json, ']');
|
strbuf_append_char(json, ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,6 +847,7 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
|
|||||||
int current_depth)
|
int current_depth)
|
||||||
{
|
{
|
||||||
int comma, keytype, json_pos, err;
|
int comma, keytype, json_pos, err;
|
||||||
|
int has_items = 0;
|
||||||
strbuf_t *json = ctx->json;
|
strbuf_t *json = ctx->json;
|
||||||
|
|
||||||
/* Object */
|
/* Object */
|
||||||
@@ -807,12 +857,17 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
|
|||||||
/* table, startkey */
|
/* table, startkey */
|
||||||
comma = 0;
|
comma = 0;
|
||||||
while (lua_next(l, -2) != 0) {
|
while (lua_next(l, -2) != 0) {
|
||||||
|
has_items = 1;
|
||||||
|
|
||||||
json_pos = strbuf_length(json);
|
json_pos = strbuf_length(json);
|
||||||
if (comma++ > 0)
|
if (comma++ > 0)
|
||||||
strbuf_append_char(json, ',');
|
strbuf_append_char(json, ',');
|
||||||
else
|
else
|
||||||
comma = 1;
|
comma = 1;
|
||||||
|
|
||||||
|
if (ctx->options->indent)
|
||||||
|
json_append_newline_and_indent(json, ctx, current_depth);
|
||||||
|
|
||||||
/* table, key, value */
|
/* table, key, value */
|
||||||
keytype = lua_type(l, -2);
|
keytype = lua_type(l, -2);
|
||||||
if (keytype == LUA_TNUMBER) {
|
if (keytype == LUA_TNUMBER) {
|
||||||
@@ -827,6 +882,8 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
|
|||||||
"table key must be a number or string");
|
"table key must be a number or string");
|
||||||
/* never returns */
|
/* never returns */
|
||||||
}
|
}
|
||||||
|
if (ctx->options->indent)
|
||||||
|
strbuf_append_char(json, ' ');
|
||||||
|
|
||||||
/* table, key, value */
|
/* table, key, value */
|
||||||
err = json_append_data(l, ctx, current_depth);
|
err = json_append_data(l, ctx, current_depth);
|
||||||
@@ -841,6 +898,9 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
|
|||||||
/* table, key */
|
/* table, key */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (has_items && ctx->options->indent)
|
||||||
|
json_append_newline_and_indent(json, ctx, current_depth-1);
|
||||||
|
|
||||||
strbuf_append_char(json, '}');
|
strbuf_append_char(json, '}');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,7 +1026,10 @@ static int json_append_data(lua_State *l, json_encode_t *ctx,
|
|||||||
static int json_encode(lua_State *l)
|
static int json_encode(lua_State *l)
|
||||||
{
|
{
|
||||||
json_config_t *cfg = json_fetch_config(l);
|
json_config_t *cfg = json_fetch_config(l);
|
||||||
json_encode_options_t options = { .char2escape = { char2escape } };
|
json_encode_options_t options = {
|
||||||
|
.char2escape = { char2escape },
|
||||||
|
.indent = DEFAULT_ENCODE_INDENT,
|
||||||
|
};
|
||||||
json_encode_t ctx = { .options = &options, .cfg = cfg };
|
json_encode_t ctx = { .options = &options, .cfg = cfg };
|
||||||
strbuf_t local_encode_buf;
|
strbuf_t local_encode_buf;
|
||||||
strbuf_t *encode_buf;
|
strbuf_t *encode_buf;
|
||||||
@@ -979,26 +1042,29 @@ static int json_encode(lua_State *l)
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
luaL_checktype(l, 2, LUA_TTABLE);
|
luaL_checktype(l, 2, LUA_TTABLE);
|
||||||
|
|
||||||
lua_getfield(l, 2, "escape_slash");
|
lua_getfield(l, 2, "escape_slash");
|
||||||
|
if (!lua_isnil(l, -1)) {
|
||||||
/* We only handle the escape_slash option for now */
|
|
||||||
if (lua_isnil(l, -1)) {
|
|
||||||
lua_pop(l, 2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
luaL_checktype(l, -1, LUA_TBOOLEAN);
|
luaL_checktype(l, -1, LUA_TBOOLEAN);
|
||||||
|
|
||||||
int escape_slash = lua_toboolean(l, -1);
|
int escape_slash = lua_toboolean(l, -1);
|
||||||
|
|
||||||
if (escape_slash) {
|
if (escape_slash) {
|
||||||
/* This can be optimised by adding a new hard-coded escape table for this case,
|
/* This can be optimised by adding a new hard-coded escape table for this case,
|
||||||
* but this path will rarely if ever be used, so let's just memcpy.*/
|
* but this path will rarely if ever be used, so let's just memcpy. */
|
||||||
memcpy(customChar2escape, char2escape, sizeof(char2escape));
|
memcpy(customChar2escape, char2escape, sizeof(char2escape));
|
||||||
customChar2escape['/'] = "\\/";
|
customChar2escape['/'] = "\\/";
|
||||||
*ctx.options->char2escape = customChar2escape;
|
*ctx.options->char2escape = customChar2escape;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
lua_pop(l, 1);
|
||||||
|
|
||||||
|
lua_getfield(l, 2, "indent");
|
||||||
|
if (!lua_isnil(l, -1)) {
|
||||||
|
options.indent = luaL_checkstring(l, -1);
|
||||||
|
if (options.indent[0] == '\0') options.indent = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Also pop the opts table */
|
||||||
lua_pop(l, 2);
|
lua_pop(l, 2);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1710,6 +1776,7 @@ int lua_cjson_new(lua_State *l)
|
|||||||
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
|
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
|
||||||
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
|
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
|
||||||
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
|
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
|
||||||
|
{ "encode_indent", json_cfg_encode_indent },
|
||||||
*/
|
*/
|
||||||
{ "new", lua_cjson_new },
|
{ "new", lua_cjson_new },
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
|
@@ -192,6 +192,41 @@ describe('vim.json.encode()', function()
|
|||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('indent', function()
|
||||||
|
eq('"Test"', exec_lua([[return vim.json.encode('Test', { indent = " " })]]))
|
||||||
|
eq('[]', exec_lua([[return vim.json.encode({}, { indent = " " })]]))
|
||||||
|
eq('{}', exec_lua([[return vim.json.encode(vim.empty_dict(), { indent = " " })]]))
|
||||||
|
eq(
|
||||||
|
'[\n {\n "a": "a"\n },\n {\n "b": "b"\n }\n]',
|
||||||
|
exec_lua([[return vim.json.encode({ { a = "a" }, { b = "b" } }, { indent = " " })]])
|
||||||
|
)
|
||||||
|
eq(
|
||||||
|
'{\n "a": {\n "b": 1\n }\n}',
|
||||||
|
exec_lua([[return vim.json.encode({ a = { b = 1 } }, { indent = " " })]])
|
||||||
|
)
|
||||||
|
eq(
|
||||||
|
'[{"a":"a"},{"b":"b"}]',
|
||||||
|
exec_lua([[return vim.json.encode({ { a = "a" }, { b = "b" } }, { indent = "" })]])
|
||||||
|
)
|
||||||
|
eq(
|
||||||
|
'[\n [\n 1,\n 2\n ],\n [\n 3,\n 4\n ]\n]',
|
||||||
|
exec_lua([[return vim.json.encode({ { 1, 2 }, { 3, 4 } }, { indent = " " })]])
|
||||||
|
)
|
||||||
|
eq(
|
||||||
|
'{\nabc"a": {\nabcabc"b": 1\nabc}\n}',
|
||||||
|
exec_lua([[return vim.json.encode({ a = { b = 1 } }, { indent = "abc" })]])
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Checks for for global side-effects
|
||||||
|
eq(
|
||||||
|
'[{"a":"a"},{"b":"b"}]',
|
||||||
|
exec_lua([[
|
||||||
|
vim.json.encode('', { indent = " " })
|
||||||
|
return vim.json.encode({ { a = "a" }, { b = "b" } })
|
||||||
|
]])
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
it('dumps strings', function()
|
it('dumps strings', function()
|
||||||
eq('"Test"', exec_lua([[return vim.json.encode('Test')]]))
|
eq('"Test"', exec_lua([[return vim.json.encode('Test')]]))
|
||||||
eq('""', exec_lua([[return vim.json.encode('')]]))
|
eq('""', exec_lua([[return vim.json.encode('')]]))
|
||||||
|
Reference in New Issue
Block a user