diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index c4312c8e5d..33451910b2 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -3372,6 +3372,9 @@ vim.json.encode({obj}, {opts}) *vim.json.encode()* • {opts} (`table?`) Options table with keys: • escape_slash: (boolean) (default false) Escape slash 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`) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index a58b64f016..0e870b261f 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -262,6 +262,7 @@ LUA • |vim.list.unique()| to deduplicate lists. • |vim.list.bisect()| for binary search. • Experimental `vim.pos` and `vim.range` for Position/Range abstraction. +• |vim.json.encode()| has an `indent` option for pretty-formatting. OPTIONS diff --git a/runtime/lua/vim/_meta/json.lua b/runtime/lua/vim/_meta/json.lua index 0d59de5fa6..8a58b5ec07 100644 --- a/runtime/lua/vim/_meta/json.lua +++ b/runtime/lua/vim/_meta/json.lua @@ -38,5 +38,7 @@ function vim.json.decode(str, opts) end ---@param opts? table Options table with keys: --- - escape_slash: (boolean) (default false) Escape slash --- 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 function vim.json.encode(obj, opts) end diff --git a/src/cjson/lua_cjson.c b/src/cjson/lua_cjson.c index 0acdfab2e3..340a220835 100644 --- a/src/cjson/lua_cjson.c +++ b/src/cjson/lua_cjson.c @@ -87,6 +87,7 @@ #define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0 #define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1 #define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0 +#define DEFAULT_ENCODE_INDENT NULL #ifdef DISABLE_INVALID_NUMBERS #undef DEFAULT_DECODE_INVALID_NUMBERS @@ -168,6 +169,7 @@ typedef struct { int encode_keep_buffer; int encode_empty_table_as_object; int encode_escape_forward_slash; + const char *encode_indent; int decode_invalid_numbers; int decode_max_depth; @@ -177,6 +179,7 @@ typedef struct { typedef struct { const char **char2escape[256]; + const char *indent; } json_encode_options_t; 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: * convert: Convert extremely sparse arrays into objects? Otherwise error. * 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) 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->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH; cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES; + cfg->encode_indent = DEFAULT_ENCODE_INDENT; #if DEFAULT_ENCODE_KEEP_BUFFER > 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, 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: * - lua_State * - 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 comma, i, json_pos, err; + int has_items = 0; strbuf_t *json = ctx->json; strbuf_append_char(json, '['); comma = 0; for (i = 1; i <= array_length; i++) { + has_items = 1; + json_pos = strbuf_length(json); if (comma++ > 0) strbuf_append_char(json, ','); + + if (ctx->options->indent) + json_append_newline_and_indent(json, ctx, current_depth); + if (raw) { lua_rawgeti(l, -1, i); } else { @@ -742,6 +788,9 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept lua_pop(l, 1); } + if (has_items && ctx->options->indent) + json_append_newline_and_indent(json, ctx, current_depth-1); + strbuf_append_char(json, ']'); } @@ -798,6 +847,7 @@ static void json_append_object(lua_State *l, json_encode_t *ctx, int current_depth) { int comma, keytype, json_pos, err; + int has_items = 0; strbuf_t *json = ctx->json; /* Object */ @@ -807,12 +857,17 @@ static void json_append_object(lua_State *l, json_encode_t *ctx, /* table, startkey */ comma = 0; while (lua_next(l, -2) != 0) { + has_items = 1; + json_pos = strbuf_length(json); if (comma++ > 0) strbuf_append_char(json, ','); else comma = 1; + if (ctx->options->indent) + json_append_newline_and_indent(json, ctx, current_depth); + /* table, key, value */ keytype = lua_type(l, -2); 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"); /* never returns */ } + if (ctx->options->indent) + strbuf_append_char(json, ' '); /* table, key, value */ 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 */ } + if (has_items && ctx->options->indent) + json_append_newline_and_indent(json, ctx, current_depth-1); + 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) { 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 }; strbuf_t local_encode_buf; strbuf_t *encode_buf; @@ -979,26 +1042,29 @@ static int json_encode(lua_State *l) break; case 2: luaL_checktype(l, 2, LUA_TTABLE); + lua_getfield(l, 2, "escape_slash"); + if (!lua_isnil(l, -1)) { + luaL_checktype(l, -1, LUA_TBOOLEAN); - /* 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); - - int escape_slash = lua_toboolean(l, -1); - - if (escape_slash) { - /* 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.*/ - memcpy(customChar2escape, char2escape, sizeof(char2escape)); - customChar2escape['/'] = "\\/"; - *ctx.options->char2escape = customChar2escape; + int escape_slash = lua_toboolean(l, -1); + if (escape_slash) { + /* 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. */ + memcpy(customChar2escape, char2escape, sizeof(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); break; default: @@ -1710,6 +1776,7 @@ int lua_cjson_new(lua_State *l) { "decode_invalid_numbers", json_cfg_decode_invalid_numbers }, { "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash }, { "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types }, + { "encode_indent", json_cfg_encode_indent }, */ { "new", lua_cjson_new }, { NULL, NULL } diff --git a/test/functional/lua/json_spec.lua b/test/functional/lua/json_spec.lua index be13b4e2e4..95c828bcb2 100644 --- a/test/functional/lua/json_spec.lua +++ b/test/functional/lua/json_spec.lua @@ -192,6 +192,41 @@ describe('vim.json.encode()', function() ) 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() eq('"Test"', exec_lua([[return vim.json.encode('Test')]])) eq('""', exec_lua([[return vim.json.encode('')]]))