Skip to content

Commit 91e7b72

Browse files
committed
feat(json): support pretty-formatting with vim.json.encode()
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.
1 parent 1e1619d commit 91e7b72

File tree

5 files changed

+151
-16
lines changed

5 files changed

+151
-16
lines changed

runtime/doc/lua.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3375,6 +3375,10 @@ vim.json.encode({obj}, {opts}) *vim.json.encode()*
33753375
{opts} (`table<string,any>?`) Options table with keys:
33763376
• escape_slash: (boolean) (default false) Escape slash
33773377
characters "/" in string values.
3378+
• indent: (string) (default "") String used for indentation at
3379+
each nesting level. If non-empty enables newlines and a
3380+
space after colons. Must contain only spaces, tabs, line
3381+
feeds, or carriage returns.
33783382

33793383
Return: ~
33803384
(`string`)

runtime/doc/news.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ LUA
247247
|vim.list.unique()| to deduplicate lists.
248248
|vim.list.bisect()| for binary search.
249249
• Experimental `vim.pos` and `vim.range` for Position/Range abstraction.
250+
|vim.json.encode()| has an `indent` option for pretty-formatting.
250251

251252
OPTIONS
252253

runtime/lua/vim/_meta/json.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,8 @@ function vim.json.decode(str, opts) end
3838
---@param opts? table<string,any> Options table with keys:
3939
--- - escape_slash: (boolean) (default false) Escape slash
4040
--- characters "/" in string values.
41+
--- - indent: (string) (default "") String used for indentation at each nesting level.
42+
--- If non-empty enables newlines and a space after colons.
43+
--- Must contain only spaces, tabs, line feeds, or carriage returns.
4144
---@return string
4245
function vim.json.encode(obj, opts) end

src/cjson/lua_cjson.c

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
#define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0
8888
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
8989
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
90+
#define DEFAULT_ENCODE_INDENT NULL
9091

9192
#ifdef DISABLE_INVALID_NUMBERS
9293
#undef DEFAULT_DECODE_INVALID_NUMBERS
@@ -168,6 +169,7 @@ typedef struct {
168169
int encode_keep_buffer;
169170
int encode_empty_table_as_object;
170171
int encode_escape_forward_slash;
172+
const char *encode_indent;
171173

172174
int decode_invalid_numbers;
173175
int decode_max_depth;
@@ -177,6 +179,7 @@ typedef struct {
177179

178180
typedef struct {
179181
const char **char2escape[256];
182+
const char *indent;
180183
} json_encode_options_t;
181184

182185
typedef struct {
@@ -330,6 +333,20 @@ static int json_enum_option(lua_State *l, int optindex, int *setting,
330333
}
331334
*/
332335

336+
/* Process string option for a configuration function */
337+
/*
338+
static int json_string_option(lua_State *l, int optindex, const char **setting)
339+
{
340+
if (!lua_isnil(l, optindex)) {
341+
const char *value = luaL_checkstring(l, optindex);
342+
*setting = value;
343+
}
344+
345+
lua_pushstring(l, *setting ? *setting : "");
346+
return 1;
347+
}
348+
*/
349+
333350
/* Configures handling of extremely sparse arrays:
334351
* convert: Convert extremely sparse arrays into objects? Otherwise error.
335352
* ratio: 0: always allow sparse; 1: never allow sparse; >1: use ratio
@@ -436,6 +453,41 @@ static int json_cfg_encode_keep_buffer(lua_State *l)
436453
}
437454
*/
438455

456+
/* Returns nonzero if the string consists only of allowed whitespace characters */
457+
static int is_allowed_ws(const char *s) {
458+
if (!s) return 0;
459+
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
460+
switch (*p) {
461+
case 0x20: /* space */
462+
case 0x09: /* tab */
463+
case 0x0A: /* newline */
464+
case 0x0D: /* carriage return */
465+
break;
466+
default:
467+
return 0;
468+
}
469+
}
470+
return 1;
471+
}
472+
473+
/* Configure how to indent output */
474+
/*
475+
static int json_cfg_encode_indent(lua_State *l)
476+
{
477+
json_config_t *cfg = json_arg_init(l, 1);
478+
479+
const char *indent = cfg->encode_indent;
480+
json_string_option(l, 1, &indent);
481+
if (indent[0] == '\0') indent = NULL;
482+
else if (!is_allowed_ws(indent)) {
483+
luaL_argerror(l, 1, "indent must contain only spaces, tabs, line feeds, or carriage returns");
484+
}
485+
cfg->encode_indent = indent;
486+
487+
return 1;
488+
}
489+
*/
490+
439491
#if defined(DISABLE_INVALID_NUMBERS) && !defined(USE_INTERNAL_FPCONV)
440492
void json_verify_invalid_number_setting(lua_State *l, int *setting)
441493
{
@@ -533,6 +585,7 @@ static void json_create_config(lua_State *l)
533585
cfg->decode_array_with_array_mt = DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT;
534586
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
535587
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
588+
cfg->encode_indent = DEFAULT_ENCODE_INDENT;
536589

537590
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
538591
strbuf_init(&cfg->encode_buf, 0);
@@ -704,6 +757,13 @@ static void json_check_encode_depth(lua_State *l, json_config_t *cfg,
704757
static int json_append_data(lua_State *l, json_encode_t *cfg,
705758
int current_depth);
706759

760+
static void json_append_newline_and_indent(strbuf_t *json, json_encode_t *ctx, int depth)
761+
{
762+
strbuf_append_char(json, '\n');
763+
for (int i = 0; i < depth; i++)
764+
strbuf_append_string(json, ctx->options->indent);
765+
}
766+
707767
/* json_append_array args:
708768
* - lua_State
709769
* - JSON strbuf
@@ -712,15 +772,22 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept
712772
int array_length, int raw)
713773
{
714774
int comma, i, json_pos, err;
775+
int has_items = 0;
715776
strbuf_t *json = ctx->json;
716777

717778
strbuf_append_char(json, '[');
718779

719780
comma = 0;
720781
for (i = 1; i <= array_length; i++) {
782+
has_items = 1;
783+
721784
json_pos = strbuf_length(json);
722785
if (comma++ > 0)
723786
strbuf_append_char(json, ',');
787+
788+
if (ctx->options->indent)
789+
json_append_newline_and_indent(json, ctx, current_depth);
790+
724791
if (raw) {
725792
lua_rawgeti(l, -1, i);
726793
} else {
@@ -742,6 +809,9 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept
742809
lua_pop(l, 1);
743810
}
744811

812+
if (has_items && ctx->options->indent)
813+
json_append_newline_and_indent(json, ctx, current_depth-1);
814+
745815
strbuf_append_char(json, ']');
746816
}
747817

@@ -798,6 +868,7 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
798868
int current_depth)
799869
{
800870
int comma, keytype, json_pos, err;
871+
int has_items = 0;
801872
strbuf_t *json = ctx->json;
802873

803874
/* Object */
@@ -807,12 +878,17 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
807878
/* table, startkey */
808879
comma = 0;
809880
while (lua_next(l, -2) != 0) {
881+
has_items = 1;
882+
810883
json_pos = strbuf_length(json);
811884
if (comma++ > 0)
812885
strbuf_append_char(json, ',');
813886
else
814887
comma = 1;
815888

889+
if (ctx->options->indent)
890+
json_append_newline_and_indent(json, ctx, current_depth);
891+
816892
/* table, key, value */
817893
keytype = lua_type(l, -2);
818894
if (keytype == LUA_TNUMBER) {
@@ -827,6 +903,8 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
827903
"table key must be a number or string");
828904
/* never returns */
829905
}
906+
if (ctx->options->indent)
907+
strbuf_append_char(json, ' ');
830908

831909
/* table, key, value */
832910
err = json_append_data(l, ctx, current_depth);
@@ -841,6 +919,9 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
841919
/* table, key */
842920
}
843921

922+
if (has_items && ctx->options->indent)
923+
json_append_newline_and_indent(json, ctx, current_depth-1);
924+
844925
strbuf_append_char(json, '}');
845926
}
846927

@@ -966,7 +1047,10 @@ static int json_append_data(lua_State *l, json_encode_t *ctx,
9661047
static int json_encode(lua_State *l)
9671048
{
9681049
json_config_t *cfg = json_fetch_config(l);
969-
json_encode_options_t options = { .char2escape = { char2escape } };
1050+
json_encode_options_t options = {
1051+
.char2escape = { char2escape },
1052+
.indent = DEFAULT_ENCODE_INDENT,
1053+
};
9701054
json_encode_t ctx = { .options = &options, .cfg = cfg };
9711055
strbuf_t local_encode_buf;
9721056
strbuf_t *encode_buf;
@@ -979,26 +1063,31 @@ static int json_encode(lua_State *l)
9791063
break;
9801064
case 2:
9811065
luaL_checktype(l, 2, LUA_TTABLE);
982-
lua_getfield(l, 2, "escape_slash");
9831066

984-
/* We only handle the escape_slash option for now */
985-
if (lua_isnil(l, -1)) {
986-
lua_pop(l, 2);
987-
break;
1067+
lua_getfield(l, 2, "escape_slash");
1068+
if (!lua_isnil(l, -1)) {
1069+
luaL_checktype(l, -1, LUA_TBOOLEAN);
1070+
1071+
int escape_slash = lua_toboolean(l, -1);
1072+
if (escape_slash) {
1073+
/* This can be optimised by adding a new hard-coded escape table for this case,
1074+
* but this path will rarely if ever be used, so let's just memcpy. */
1075+
memcpy(customChar2escape, char2escape, sizeof(char2escape));
1076+
customChar2escape['/'] = "\\/";
1077+
*ctx.options->char2escape = customChar2escape;
1078+
}
9881079
}
1080+
lua_pop(l, 1);
9891081

990-
luaL_checktype(l, -1, LUA_TBOOLEAN);
991-
992-
int escape_slash = lua_toboolean(l, -1);
993-
994-
if (escape_slash) {
995-
/* This can be optimised by adding a new hard-coded escape table for this case,
996-
* but this path will rarely if ever be used, so let's just memcpy.*/
997-
memcpy(customChar2escape, char2escape, sizeof(char2escape));
998-
customChar2escape['/'] = "\\/";
999-
*ctx.options->char2escape = customChar2escape;
1082+
lua_getfield(l, 2, "indent");
1083+
if (!lua_isnil(l, -1)) {
1084+
options.indent = luaL_checkstring(l, -1);
1085+
if (options.indent[0] == '\0') options.indent = NULL;
1086+
else if (!is_allowed_ws(options.indent))
1087+
luaL_error(l, "indent must contain only spaces, tabs, line feeds, or carriage returns");
10001088
}
10011089

1090+
/* Also pop the opts table */
10021091
lua_pop(l, 2);
10031092
break;
10041093
default:
@@ -1710,6 +1799,7 @@ int lua_cjson_new(lua_State *l)
17101799
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
17111800
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
17121801
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
1802+
{ "encode_indent", json_cfg_encode_indent },
17131803
*/
17141804
{ "new", lua_cjson_new },
17151805
{ NULL, NULL }

test/functional/lua/json_spec.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,43 @@ describe('vim.json.encode()', function()
192192
)
193193
end)
194194

195+
it('indent', function()
196+
eq('"Test"', exec_lua([[return vim.json.encode('Test', { indent = " " })]]))
197+
eq('[]', exec_lua([[return vim.json.encode({}, { indent = " " })]]))
198+
eq('{}', exec_lua([[return vim.json.encode(vim.empty_dict(), { indent = " " })]]))
199+
eq(
200+
'[\n {\n "a": "a"\n },\n {\n "b": "b"\n }\n]',
201+
exec_lua([[return vim.json.encode({ { a = "a" }, { b = "b" } }, { indent = " " })]])
202+
)
203+
eq(
204+
'[\n\t{\n\t\t"a": "a"\n\t},\n\t{\n\t\t"b": "b"\n\t}\n]',
205+
exec_lua([[return vim.json.encode({ { a = "a" }, { b = "b" } }, { indent = "\t" })]])
206+
)
207+
eq(
208+
'[{"a":"a"},{"b":"b"}]',
209+
exec_lua([[return vim.json.encode({ { a = "a" }, { b = "b" } }, { indent = "" })]])
210+
)
211+
212+
-- Test validation
213+
eq(
214+
'indent must contain only spaces, tabs, line feeds, or carriage returns',
215+
pcall_err(exec_lua, [[return vim.json.encode({}, { indent = "abc" })]])
216+
)
217+
eq(
218+
'indent must contain only spaces, tabs, line feeds, or carriage returns',
219+
pcall_err(exec_lua, [[return vim.json.encode({}, { indent = "\t\r1" })]])
220+
)
221+
222+
-- Checks for for global side-effects
223+
eq(
224+
'[{"a":"a"},{"b":"b"}]',
225+
exec_lua([[
226+
vim.json.encode('', { indent = " " })
227+
return vim.json.encode({ { a = "a" }, { b = "b" } })
228+
]])
229+
)
230+
end)
231+
195232
it('dumps strings', function()
196233
eq('"Test"', exec_lua([[return vim.json.encode('Test')]]))
197234
eq('""', exec_lua([[return vim.json.encode('')]]))

0 commit comments

Comments
 (0)