From 040b487af0710b3df337ca139d960c82260fc73c Mon Sep 17 00:00:00 2001 From: Harsh Kapse Date: Thu, 13 Aug 2026 14:15:59 +0530 Subject: [PATCH] feat(treesitter): add lua rules and testdata --- internal/treesitter/lua/rules.yml | 112 + internal/treesitter/parser_test.go | 11 + internal/treesitter/rules_test.go | 11 + .../lua_kong_add_missing_select/expected.json | 6939 +++++++++++++++++ .../lua_kong_add_missing_select/metadata.json | 12 + .../lua_kong_add_missing_select/new.lua | 1545 ++++ .../lua_kong_add_missing_select/old.lua | 1522 ++++ .../lua_kong_check_timer_return/expected.json | 2213 ++++++ .../lua_kong_check_timer_return/metadata.json | 12 + .../lua_kong_check_timer_return/new.lua | 457 ++ .../lua_kong_check_timer_return/old.lua | 447 ++ .../lua_kong_comment_docs_cors/expected.json | 284 + .../lua_kong_comment_docs_cors/metadata.json | 12 + .../lua_kong_comment_docs_cors/new.lua | 54 + .../lua_kong_comment_docs_cors/old.lua | 54 + .../expected.json | 1905 +++++ .../metadata.json | 12 + .../lua_kong_dp_status_ready_rpc/new.lua | 215 + .../lua_kong_dp_status_ready_rpc/old.lua | 169 + .../expected.json | 2141 +++++ .../metadata.json | 12 + .../lua_kong_enable_rpc_sync_tests/new.lua | 381 + .../lua_kong_enable_rpc_sync_tests/old.lua | 373 + .../expected.json | 2889 +++++++ .../metadata.json | 12 + .../new.lua | 518 ++ .../old.lua | 513 ++ .../lua_kong_sync_retry_timeout/expected.json | 3341 ++++++++ .../lua_kong_sync_retry_timeout/metadata.json | 12 + .../lua_kong_sync_retry_timeout/new.lua | 473 ++ .../lua_kong_sync_retry_timeout/old.lua | 469 ++ .../lua_neovim_buf_func_scalar/expected.json | 1400 ++++ .../lua_neovim_buf_func_scalar/metadata.json | 12 + .../lua_neovim_buf_func_scalar/new.lua | 342 + .../lua_neovim_buf_func_scalar/old.lua | 326 + .../lua_neovim_dir_spec_change/expected.json | 3980 ++++++++++ .../lua_neovim_dir_spec_change/metadata.json | 14 + .../lua_neovim_dir_spec_change/new.lua | 711 ++ .../lua_neovim_dir_spec_change/old.lua | 701 ++ .../lua_neovim_fs_api_refactor/expected.json | 5557 +++++++++++++ .../lua_neovim_fs_api_refactor/metadata.json | 12 + .../lua_neovim_fs_api_refactor/new.lua | 1095 +++ .../lua_neovim_fs_api_refactor/old.lua | 1101 +++ .../lua_neovim_fs_api_small/expected.json | 4089 ++++++++++ .../lua_neovim_fs_api_small/metadata.json | 12 + .../testdata/lua_neovim_fs_api_small/new.lua | 968 +++ .../testdata/lua_neovim_fs_api_small/old.lua | 969 +++ .../expected.json | 6418 +++++++++++++++ .../metadata.json | 14 + .../lua_neovim_gen_help_html_change/new.lua | 1385 ++++ .../lua_neovim_gen_help_html_change/old.lua | 1374 ++++ .../lua_neovim_ui2_messages/expected.json | 3462 ++++++++ .../lua_neovim_ui2_messages/metadata.json | 12 + .../testdata/lua_neovim_ui2_messages/new.lua | 734 ++ .../testdata/lua_neovim_ui2_messages/old.lua | 730 ++ .../expected.json | 3707 +++++++++ .../metadata.json | 12 + .../lua_neovim_write_spec_refactor/new.lua | 381 + .../lua_neovim_write_spec_refactor/old.lua | 385 + .../lua_neovim_zip_spec_change/expected.json | 3279 ++++++++ .../lua_neovim_zip_spec_change/metadata.json | 16 + .../lua_neovim_zip_spec_change/new.lua | 531 ++ .../lua_neovim_zip_spec_change/old.lua | 527 ++ 63 files changed, 71376 insertions(+) create mode 100644 internal/treesitter/lua/rules.yml create mode 100644 tests/testdata/lua_kong_add_missing_select/expected.json create mode 100644 tests/testdata/lua_kong_add_missing_select/metadata.json create mode 100644 tests/testdata/lua_kong_add_missing_select/new.lua create mode 100644 tests/testdata/lua_kong_add_missing_select/old.lua create mode 100644 tests/testdata/lua_kong_check_timer_return/expected.json create mode 100644 tests/testdata/lua_kong_check_timer_return/metadata.json create mode 100644 tests/testdata/lua_kong_check_timer_return/new.lua create mode 100644 tests/testdata/lua_kong_check_timer_return/old.lua create mode 100644 tests/testdata/lua_kong_comment_docs_cors/expected.json create mode 100644 tests/testdata/lua_kong_comment_docs_cors/metadata.json create mode 100644 tests/testdata/lua_kong_comment_docs_cors/new.lua create mode 100644 tests/testdata/lua_kong_comment_docs_cors/old.lua create mode 100644 tests/testdata/lua_kong_dp_status_ready_rpc/expected.json create mode 100644 tests/testdata/lua_kong_dp_status_ready_rpc/metadata.json create mode 100644 tests/testdata/lua_kong_dp_status_ready_rpc/new.lua create mode 100644 tests/testdata/lua_kong_dp_status_ready_rpc/old.lua create mode 100644 tests/testdata/lua_kong_enable_rpc_sync_tests/expected.json create mode 100644 tests/testdata/lua_kong_enable_rpc_sync_tests/metadata.json create mode 100644 tests/testdata/lua_kong_enable_rpc_sync_tests/new.lua create mode 100644 tests/testdata/lua_kong_enable_rpc_sync_tests/old.lua create mode 100644 tests/testdata/lua_kong_fix_default_workspace_check/expected.json create mode 100644 tests/testdata/lua_kong_fix_default_workspace_check/metadata.json create mode 100644 tests/testdata/lua_kong_fix_default_workspace_check/new.lua create mode 100644 tests/testdata/lua_kong_fix_default_workspace_check/old.lua create mode 100644 tests/testdata/lua_kong_sync_retry_timeout/expected.json create mode 100644 tests/testdata/lua_kong_sync_retry_timeout/metadata.json create mode 100644 tests/testdata/lua_kong_sync_retry_timeout/new.lua create mode 100644 tests/testdata/lua_kong_sync_retry_timeout/old.lua create mode 100644 tests/testdata/lua_neovim_buf_func_scalar/expected.json create mode 100644 tests/testdata/lua_neovim_buf_func_scalar/metadata.json create mode 100644 tests/testdata/lua_neovim_buf_func_scalar/new.lua create mode 100644 tests/testdata/lua_neovim_buf_func_scalar/old.lua create mode 100644 tests/testdata/lua_neovim_dir_spec_change/expected.json create mode 100644 tests/testdata/lua_neovim_dir_spec_change/metadata.json create mode 100644 tests/testdata/lua_neovim_dir_spec_change/new.lua create mode 100644 tests/testdata/lua_neovim_dir_spec_change/old.lua create mode 100644 tests/testdata/lua_neovim_fs_api_refactor/expected.json create mode 100644 tests/testdata/lua_neovim_fs_api_refactor/metadata.json create mode 100644 tests/testdata/lua_neovim_fs_api_refactor/new.lua create mode 100644 tests/testdata/lua_neovim_fs_api_refactor/old.lua create mode 100644 tests/testdata/lua_neovim_fs_api_small/expected.json create mode 100644 tests/testdata/lua_neovim_fs_api_small/metadata.json create mode 100644 tests/testdata/lua_neovim_fs_api_small/new.lua create mode 100644 tests/testdata/lua_neovim_fs_api_small/old.lua create mode 100644 tests/testdata/lua_neovim_gen_help_html_change/expected.json create mode 100644 tests/testdata/lua_neovim_gen_help_html_change/metadata.json create mode 100644 tests/testdata/lua_neovim_gen_help_html_change/new.lua create mode 100644 tests/testdata/lua_neovim_gen_help_html_change/old.lua create mode 100644 tests/testdata/lua_neovim_ui2_messages/expected.json create mode 100644 tests/testdata/lua_neovim_ui2_messages/metadata.json create mode 100644 tests/testdata/lua_neovim_ui2_messages/new.lua create mode 100644 tests/testdata/lua_neovim_ui2_messages/old.lua create mode 100644 tests/testdata/lua_neovim_write_spec_refactor/expected.json create mode 100644 tests/testdata/lua_neovim_write_spec_refactor/metadata.json create mode 100644 tests/testdata/lua_neovim_write_spec_refactor/new.lua create mode 100644 tests/testdata/lua_neovim_write_spec_refactor/old.lua create mode 100644 tests/testdata/lua_neovim_zip_spec_change/expected.json create mode 100644 tests/testdata/lua_neovim_zip_spec_change/metadata.json create mode 100644 tests/testdata/lua_neovim_zip_spec_change/new.lua create mode 100644 tests/testdata/lua_neovim_zip_spec_change/old.lua diff --git a/internal/treesitter/lua/rules.yml b/internal/treesitter/lua/rules.yml new file mode 100644 index 0000000..76ac7fd --- /dev/null +++ b/internal/treesitter/lua/rules.yml @@ -0,0 +1,112 @@ +flattened: + - string + - comment +aliased: + # Arithmetic operators + "+": arithmetic_operator_literal + "-": arithmetic_operator_literal + "*": arithmetic_operator_literal + "/": arithmetic_operator_literal + "//": arithmetic_operator_literal + "%": arithmetic_operator_literal + "^": arithmetic_operator_literal + + # Comparison operators + "==": comparison_operator_literal + "~=": comparison_operator_literal + "<": comparison_operator_literal + "<=": comparison_operator_literal + ">": comparison_operator_literal + ">=": comparison_operator_literal + + # Logical operators + "and": logical_operator_literal + "or": logical_operator_literal + "not": unary_operator_literal + + # Bitwise operators (Lua 5.3+) + "&": bitwise_operator_literal + "|": bitwise_operator_literal + "~": bitwise_operator_literal + "<<": bitwise_operator_literal + ">>": bitwise_operator_literal + + # String concatenation + "..": concat_operator_literal + + # Assignment + "=": assignment_operator_literal + + # Length operator + "#": length_operator_literal + +ignored: + - "\n" + - "(" + - ")" + - "{" + - "}" + - "[" + - "]" + - "," + - ";" + - ":" + - "." + - "::" + +scaffolding: + - chunk + - block + - arguments + - parameters + - parenthesized_expression + - expression_list + - variable_list + - table_constructor + - field + - function_declaration + - function_definition + - if_statement + - elseif_statement + - else_statement + - for_statement + - for_generic_clause + - for_numeric_clause + - while_statement + - repeat_statement + - do_statement + - assignment_statement + - variable_declaration + - return_statement + - label_statement + - goto_statement + +keywords: + - function + - local + - return + - if + - else + - elseif + - then + - end + - for + - in + - do + - while + - repeat + - until + - break + - goto + - and + - or + - not + - true + - false + - nil + +declarations: + - function_declaration + - function_definition + - variable_declaration + diff --git a/internal/treesitter/parser_test.go b/internal/treesitter/parser_test.go index 9843df3..7593c17 100644 --- a/internal/treesitter/parser_test.go +++ b/internal/treesitter/parser_test.go @@ -207,6 +207,17 @@ int main() { std::cout << "Hello C++\n"; return 0; }`) t.Error("Parse(): expected non-nil AST root") } }) + + t.Run("valid lua source", func(t *testing.T) { + src := []byte(`local function greet(name) print("Hello, " .. name) end`) + ast, err := Parse(src, "main.lua") + if err != nil { + t.Fatalf("Parse(): unexpected error: %v", err) + } + if ast == nil { + t.Error("Parse(): expected non-nil AST root") + } + }) t.Run("unknown extension", func(t *testing.T) { src := []byte(`some text`) ast, err := Parse(src, "main.xyz") diff --git a/internal/treesitter/rules_test.go b/internal/treesitter/rules_test.go index e8cbbc1..4a059c7 100644 --- a/internal/treesitter/rules_test.go +++ b/internal/treesitter/rules_test.go @@ -146,6 +146,17 @@ func TestRulesAliased(t *testing.T) { "&", "|", "^", "<<", ">>", "~", }, }, + { + lang: "lua", + operators: []string{ + "+", "-", "*", "/", "//", "%", "^", + "==", "~=", "<", "<=", ">", ">=", + "and", "or", "not", + "=", + "&", "|", "~", "<<", ">>", + "..", "#", + }, + }, } for _, tt := range tests { diff --git a/tests/testdata/lua_kong_add_missing_select/expected.json b/tests/testdata/lua_kong_add_missing_select/expected.json new file mode 100644 index 0000000..f7db60e --- /dev/null +++ b/tests/testdata/lua_kong_add_missing_select/expected.json @@ -0,0 +1,6939 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15604 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3 + ], + "type": "block", + "start_byte": 15485, + "end_byte": 16030 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 77, + 3, + 69 + ], + "type": "comment", + "start_byte": 35644, + "end_byte": 35727 + }, + "parent": { + "tree": "after", + "path": [ + 77, + 3 + ], + "type": "block", + "start_byte": 21306, + "end_byte": 44568 + }, + "position": 69 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 77, + 3, + 70 + ], + "type": "function_call", + "start_byte": 35730, + "end_byte": 36185 + }, + "parent": { + "tree": "after", + "path": [ + 77, + 3 + ], + "type": "block", + "start_byte": 21306, + "end_byte": 44568 + }, + "position": 70, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 15519, + "end_byte": 15521 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15778 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 1 + ], + "type": "identifier", + "label": "options", + "start_byte": 15522, + "end_byte": 15529 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15778 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 15530, + "end_byte": 15534 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15778 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 3 + ], + "type": "comment", + "start_byte": 15539, + "end_byte": 15599 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15778 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 5 + ], + "type": "end", + "label": "end", + "start_byte": 15775, + "end_byte": 15778 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15778 + }, + "position": 5 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 15519, + "end_byte": 15521 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0 + ], + "type": "if_statement", + "start_byte": 15604, + "end_byte": 15772 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15620 + }, + "old_position": 0, + "dest_start_byte": 15604, + "dest_end_byte": 15606, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 15571, + "end_byte": 15575 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0 + ], + "type": "if_statement", + "start_byte": 15604, + "end_byte": 15772 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15620 + }, + "old_position": 2, + "dest_start_byte": 15622, + "dest_end_byte": 15626, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 4 + ], + "type": "end", + "label": "end", + "start_byte": 15617, + "end_byte": 15620 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0 + ], + "type": "if_statement", + "start_byte": 15604, + "end_byte": 15772 + }, + "position": 5, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15620 + }, + "old_position": 4, + "dest_start_byte": 15769, + "dest_end_byte": 15772 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 1 + ], + "type": "dot_index_expression", + "start_byte": 15607, + "end_byte": 15621 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0 + ], + "type": "if_statement", + "start_byte": 15604, + "end_byte": 15772 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 3 + ], + "type": "block", + "start_byte": 15633, + "end_byte": 15669 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0 + ], + "type": "if_statement", + "start_byte": 15604, + "end_byte": 15772 + }, + "position": 4, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4 + ], + "type": "elseif_statement", + "start_byte": 15675, + "end_byte": 15730 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0 + ], + "type": "if_statement", + "start_byte": 15604, + "end_byte": 15772 + }, + "position": 5 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4, + 0 + ], + "type": "elseif", + "label": "elseif", + "start_byte": 15675, + "end_byte": 15681 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4 + ], + "type": "elseif_statement", + "start_byte": 15675, + "end_byte": 15764 + }, + "position": 0 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 0, + 0 + ], + "type": "dot_index_expression", + "start_byte": 15522, + "end_byte": 15537 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4, + 1 + ], + "type": "binary_expression", + "start_byte": 15682, + "end_byte": 15718 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 15522, + "end_byte": 15549 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 15682, + "dest_end_byte": 15697 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 15538, + "end_byte": 15541 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4, + 1 + ], + "type": "binary_expression", + "start_byte": 15682, + "end_byte": 15718 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 15522, + "end_byte": 15549 + }, + "old_position": 1, + "dest_start_byte": 15698, + "dest_end_byte": 15701 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 15719, + "end_byte": 15723 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4 + ], + "type": "elseif_statement", + "start_byte": 15675, + "end_byte": 15764 + }, + "position": 2 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 3 + ], + "type": "block", + "start_byte": 15580, + "end_byte": 15614 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4 + ], + "type": "elseif_statement", + "start_byte": 15675, + "end_byte": 15764 + }, + "position": 3, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1 + ], + "type": "if_statement", + "start_byte": 15519, + "end_byte": 15620 + }, + "old_position": 3, + "subtree": true, + "dest_start_byte": 15730, + "dest_end_byte": 15764 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 2 + ], + "type": "dot_index_expression", + "start_byte": 15554, + "end_byte": 15570 + }, + "parent": { + "tree": "after", + "path": [ + 59, + 3, + 1, + 4, + 0, + 4, + 1 + ], + "type": "binary_expression", + "start_byte": 15682, + "end_byte": 15718 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1 + ], + "type": "binary_expression", + "start_byte": 15522, + "end_byte": 15570 + }, + "old_position": 2, + "subtree": true, + "dest_start_byte": 15702, + "dest_end_byte": 15718 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 15550, + "end_byte": 15553 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 59, + 3, + 1, + 1, + 0, + 2 + ], + "type": "identifier", + "label": "options", + "start_byte": 15542, + "end_byte": 15549 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": 376, + "right_line": 376 + }, + { + "left_line": 377, + "right_line": 377 + }, + { + "left_line": 378, + "right_line": 378 + }, + { + "left_line": 379, + "right_line": 379 + }, + { + "left_line": 380, + "right_line": 380 + }, + { + "left_line": 381, + "right_line": 381 + }, + { + "left_line": 382, + "right_line": 382 + }, + { + "left_line": 383, + "right_line": 383 + }, + { + "left_line": 384, + "right_line": 384 + }, + { + "left_line": 385, + "right_line": 385 + }, + { + "left_line": 386, + "right_line": 386 + }, + { + "left_line": 387, + "right_line": 387 + }, + { + "left_line": 388, + "right_line": 388 + }, + { + "left_line": 389, + "right_line": 389 + }, + { + "left_line": 390, + "right_line": 390 + }, + { + "left_line": 391, + "right_line": 391 + }, + { + "left_line": 392, + "right_line": 392 + }, + { + "left_line": 393, + "right_line": 393 + }, + { + "left_line": 394, + "right_line": 394 + }, + { + "left_line": 395, + "right_line": 395 + }, + { + "left_line": 396, + "right_line": 396 + }, + { + "left_line": 397, + "right_line": 397 + }, + { + "left_line": 398, + "right_line": 398 + }, + { + "left_line": 399, + "right_line": 399 + }, + { + "left_line": 400, + "right_line": 400 + }, + { + "left_line": 401, + "right_line": 401 + }, + { + "left_line": 402, + "right_line": 402 + }, + { + "left_line": 403, + "right_line": 403 + }, + { + "left_line": 404, + "right_line": 404 + }, + { + "left_line": 405, + "right_line": 405 + }, + { + "left_line": 406, + "right_line": 406 + }, + { + "left_line": 407, + "right_line": 407 + }, + { + "left_line": 408, + "right_line": 408 + }, + { + "left_line": 409, + "right_line": 409 + }, + { + "left_line": 410, + "right_line": 410 + }, + { + "left_line": 411, + "right_line": 411 + }, + { + "left_line": 412, + "right_line": 412 + }, + { + "left_line": 413, + "right_line": 413 + }, + { + "left_line": 414, + "right_line": 414 + }, + { + "left_line": 415, + "right_line": 415 + }, + { + "left_line": 416, + "right_line": 416 + }, + { + "left_line": 417, + "right_line": 417 + }, + { + "left_line": 418, + "right_line": 418 + }, + { + "left_line": 419, + "right_line": 419 + }, + { + "left_line": 420, + "right_line": 420 + }, + { + "left_line": 421, + "right_line": 421 + }, + { + "left_line": 422, + "right_line": 422 + }, + { + "left_line": 423, + "right_line": 423 + }, + { + "left_line": 424, + "right_line": 424 + }, + { + "left_line": 425, + "right_line": 425 + }, + { + "left_line": 426, + "right_line": 426 + }, + { + "left_line": 427, + "right_line": 427 + }, + { + "left_line": 428, + "right_line": 428 + }, + { + "left_line": 429, + "right_line": 429 + }, + { + "left_line": 430, + "right_line": 430 + }, + { + "left_line": 431, + "right_line": 431 + }, + { + "left_line": 432, + "right_line": 432 + }, + { + "left_line": 433, + "right_line": 433 + }, + { + "left_line": 434, + "right_line": 434 + }, + { + "left_line": 435, + "right_line": 435 + }, + { + "left_line": 436, + "right_line": 436 + }, + { + "left_line": 437, + "right_line": 437 + }, + { + "left_line": 438, + "right_line": 438 + }, + { + "left_line": 439, + "right_line": 439 + }, + { + "left_line": 440, + "right_line": 440 + }, + { + "left_line": 441, + "right_line": 441 + }, + { + "left_line": 442, + "right_line": 442 + }, + { + "left_line": 443, + "right_line": 443 + }, + { + "left_line": 444, + "right_line": 444 + }, + { + "left_line": 445, + "right_line": 445 + }, + { + "left_line": 446, + "right_line": 446 + }, + { + "left_line": 447, + "right_line": 447 + }, + { + "left_line": 448, + "right_line": 448 + }, + { + "left_line": 449, + "right_line": 449 + }, + { + "left_line": 450, + "right_line": 450 + }, + { + "left_line": 451, + "right_line": 451 + }, + { + "left_line": 452, + "right_line": 452 + }, + { + "left_line": 453, + "right_line": 453 + }, + { + "left_line": 454, + "right_line": 454 + }, + { + "left_line": 455, + "right_line": 455 + }, + { + "left_line": 456, + "right_line": 456 + }, + { + "left_line": 457, + "right_line": 457 + }, + { + "left_line": 458, + "right_line": 458 + }, + { + "left_line": 459, + "right_line": 459 + }, + { + "left_line": 460, + "right_line": 460 + }, + { + "left_line": 461, + "right_line": 461 + }, + { + "left_line": 462, + "right_line": 462 + }, + { + "left_line": 463, + "right_line": 463 + }, + { + "left_line": 464, + "right_line": 464 + }, + { + "left_line": 465, + "right_line": 465 + }, + { + "left_line": 466, + "right_line": 466 + }, + { + "left_line": 467, + "right_line": 467 + }, + { + "left_line": 468, + "right_line": 468 + }, + { + "left_line": 469, + "right_line": 469 + }, + { + "left_line": 470, + "right_line": 470 + }, + { + "left_line": 471, + "right_line": 471 + }, + { + "left_line": 472, + "right_line": 472 + }, + { + "left_line": 473, + "right_line": 473 + }, + { + "left_line": 474, + "right_line": 474 + }, + { + "left_line": 475, + "right_line": 475 + }, + { + "left_line": 476, + "right_line": 476 + }, + { + "left_line": 477, + "right_line": 477 + }, + { + "left_line": 478, + "right_line": 478 + }, + { + "left_line": 479, + "right_line": 479 + }, + { + "left_line": 480, + "right_line": 480 + }, + { + "left_line": 481, + "right_line": 481 + }, + { + "left_line": 482, + "right_line": 482 + }, + { + "left_line": 483, + "right_line": 483 + }, + { + "left_line": 484, + "right_line": 484 + }, + { + "left_line": 485, + "right_line": 485 + }, + { + "left_line": 486, + "right_line": 486 + }, + { + "left_line": 487, + "right_line": 487 + }, + { + "left_line": 488, + "right_line": 488 + }, + { + "left_line": 489, + "right_line": 489 + }, + { + "left_line": 490, + "right_line": 490 + }, + { + "left_line": 491, + "right_line": 491 + }, + { + "left_line": 492, + "right_line": 492 + }, + { + "left_line": 493, + "right_line": 493 + }, + { + "left_line": 494, + "right_line": 494 + }, + { + "left_line": 495, + "right_line": 495 + }, + { + "left_line": 496, + "right_line": 496 + }, + { + "left_line": 497, + "right_line": 497 + }, + { + "left_line": 498, + "right_line": 498 + }, + { + "left_line": 499, + "right_line": 499 + }, + { + "left_line": 500, + "right_line": 500 + }, + { + "left_line": 501, + "right_line": 501 + }, + { + "left_line": 502, + "right_line": 502 + }, + { + "left_line": 503, + "right_line": 503 + }, + { + "left_line": 504, + "right_line": 504 + }, + { + "left_line": 505, + "right_line": 505 + }, + { + "left_line": 506, + "right_line": 506 + }, + { + "left_line": 507, + "right_line": 507 + }, + { + "left_line": 508, + "right_line": 508 + }, + { + "left_line": 509, + "right_line": 509 + }, + { + "left_line": 510, + "right_line": 510 + }, + { + "left_line": 511, + "right_line": 511 + }, + { + "left_line": 512, + "right_line": 512 + }, + { + "left_line": 513, + "right_line": 513 + }, + { + "left_line": 514, + "right_line": 514 + }, + { + "left_line": 515, + "right_line": 515 + }, + { + "left_line": 516, + "right_line": 516 + }, + { + "left_line": 517, + "right_line": 517 + }, + { + "left_line": 518, + "right_line": 518 + }, + { + "left_line": 519, + "right_line": 519 + }, + { + "left_line": 520, + "right_line": 520 + }, + { + "left_line": 521, + "right_line": 521 + }, + { + "left_line": 522, + "right_line": 522 + }, + { + "left_line": 523, + "right_line": 523 + }, + { + "left_line": 524, + "right_line": 524 + }, + { + "left_line": 525, + "right_line": 525 + }, + { + "left_line": 526, + "right_line": 526 + }, + { + "left_line": 527, + "right_line": 527 + }, + { + "left_line": 528, + "right_line": 528 + }, + { + "left_line": 529, + "right_line": 529 + }, + { + "left_line": 530, + "right_line": 530 + }, + { + "left_line": 531, + "right_line": 531 + }, + { + "left_line": 532, + "right_line": 532 + }, + { + "left_line": 533, + "right_line": 533 + }, + { + "left_line": 534, + "right_line": 534 + }, + { + "left_line": 535, + "right_line": 535 + }, + { + "left_line": 536, + "right_line": 536 + }, + { + "left_line": 537, + "right_line": 537 + }, + { + "left_line": 538, + "right_line": 538 + }, + { + "left_line": 539, + "right_line": 539 + }, + { + "left_line": 540, + "right_line": 540 + }, + { + "left_line": 541, + "right_line": 541 + }, + { + "left_line": 542, + "right_line": 542 + }, + { + "left_line": 543, + "right_line": 543 + }, + { + "left_line": 544, + "right_line": 544 + }, + { + "left_line": 545, + "right_line": 545 + }, + { + "left_line": 546, + "right_line": 546 + }, + { + "left_line": 547, + "right_line": 547 + }, + { + "left_line": 548, + "right_line": 548 + }, + { + "left_line": 549, + "right_line": 549 + }, + { + "left_line": 550, + "right_line": 550 + }, + { + "left_line": 551, + "right_line": 551 + }, + { + "left_line": 552, + "right_line": 552 + }, + { + "left_line": 553, + "right_line": 553 + }, + { + "left_line": 554, + "right_line": 554 + }, + { + "left_line": 555, + "right_line": 555 + }, + { + "left_line": 556, + "right_line": 556 + }, + { + "left_line": 557, + "right_line": 557 + }, + { + "left_line": 558, + "right_line": 558 + }, + { + "left_line": 559, + "right_line": 559 + }, + { + "left_line": 560, + "right_line": 560 + }, + { + "left_line": 561, + "right_line": 561 + }, + { + "left_line": 562, + "right_line": 562 + }, + { + "left_line": 563, + "right_line": 563 + }, + { + "left_line": 564, + "right_line": 564 + }, + { + "left_line": 565, + "right_line": 565 + }, + { + "left_line": 566, + "right_line": 566 + }, + { + "left_line": 567, + "right_line": 567 + }, + { + "left_line": 568, + "right_line": 568 + }, + { + "left_line": 569, + "right_line": 569 + }, + { + "left_line": 570, + "right_line": 570 + }, + { + "left_line": 571, + "right_line": 571 + }, + { + "left_line": 572, + "right_line": -1 + }, + { + "left_line": 573, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 572 + }, + { + "left_line": -1, + "right_line": 573 + }, + { + "left_line": -1, + "right_line": 574 + }, + { + "left_line": -1, + "right_line": 575 + }, + { + "left_line": -1, + "right_line": 576 + }, + { + "left_line": -1, + "right_line": 577 + }, + { + "left_line": -1, + "right_line": 578 + }, + { + "left_line": -1, + "right_line": 579 + }, + { + "left_line": 574, + "right_line": 580 + }, + { + "left_line": 575, + "right_line": 581 + }, + { + "left_line": 576, + "right_line": 582 + }, + { + "left_line": 577, + "right_line": 583 + }, + { + "left_line": 578, + "right_line": 584 + }, + { + "left_line": 579, + "right_line": 585 + }, + { + "left_line": 580, + "right_line": 586 + }, + { + "left_line": 581, + "right_line": 587 + }, + { + "left_line": 582, + "right_line": 588 + }, + { + "left_line": 583, + "right_line": 589 + }, + { + "left_line": 584, + "right_line": 590 + }, + { + "left_line": 585, + "right_line": 591 + }, + { + "left_line": 586, + "right_line": 592 + }, + { + "left_line": 587, + "right_line": 593 + }, + { + "left_line": 588, + "right_line": 594 + }, + { + "left_line": 589, + "right_line": 595 + }, + { + "left_line": 590, + "right_line": 596 + }, + { + "left_line": 591, + "right_line": 597 + }, + { + "left_line": 592, + "right_line": 598 + }, + { + "left_line": 593, + "right_line": 599 + }, + { + "left_line": 594, + "right_line": 600 + }, + { + "left_line": 595, + "right_line": 601 + }, + { + "left_line": 596, + "right_line": 602 + }, + { + "left_line": 597, + "right_line": 603 + }, + { + "left_line": 598, + "right_line": 604 + }, + { + "left_line": 599, + "right_line": 605 + }, + { + "left_line": 600, + "right_line": 606 + }, + { + "left_line": 601, + "right_line": 607 + }, + { + "left_line": 602, + "right_line": 608 + }, + { + "left_line": 603, + "right_line": 609 + }, + { + "left_line": 604, + "right_line": 610 + }, + { + "left_line": 605, + "right_line": 611 + }, + { + "left_line": 606, + "right_line": 612 + }, + { + "left_line": 607, + "right_line": 613 + }, + { + "left_line": 608, + "right_line": 614 + }, + { + "left_line": 609, + "right_line": 615 + }, + { + "left_line": 610, + "right_line": 616 + }, + { + "left_line": 611, + "right_line": 617 + }, + { + "left_line": 612, + "right_line": 618 + }, + { + "left_line": 613, + "right_line": 619 + }, + { + "left_line": 614, + "right_line": 620 + }, + { + "left_line": 615, + "right_line": 621 + }, + { + "left_line": 616, + "right_line": 622 + }, + { + "left_line": 617, + "right_line": 623 + }, + { + "left_line": 618, + "right_line": 624 + }, + { + "left_line": 619, + "right_line": 625 + }, + { + "left_line": 620, + "right_line": 626 + }, + { + "left_line": 621, + "right_line": 627 + }, + { + "left_line": 622, + "right_line": 628 + }, + { + "left_line": 623, + "right_line": 629 + }, + { + "left_line": 624, + "right_line": 630 + }, + { + "left_line": 625, + "right_line": 631 + }, + { + "left_line": 626, + "right_line": 632 + }, + { + "left_line": 627, + "right_line": 633 + }, + { + "left_line": 628, + "right_line": 634 + }, + { + "left_line": 629, + "right_line": 635 + }, + { + "left_line": 630, + "right_line": 636 + }, + { + "left_line": 631, + "right_line": 637 + }, + { + "left_line": 632, + "right_line": 638 + }, + { + "left_line": 633, + "right_line": 639 + }, + { + "left_line": 634, + "right_line": 640 + }, + { + "left_line": 635, + "right_line": 641 + }, + { + "left_line": 636, + "right_line": 642 + }, + { + "left_line": 637, + "right_line": 643 + }, + { + "left_line": 638, + "right_line": 644 + }, + { + "left_line": 639, + "right_line": 645 + }, + { + "left_line": 640, + "right_line": 646 + }, + { + "left_line": 641, + "right_line": 647 + }, + { + "left_line": 642, + "right_line": 648 + }, + { + "left_line": 643, + "right_line": 649 + }, + { + "left_line": 644, + "right_line": 650 + }, + { + "left_line": 645, + "right_line": 651 + }, + { + "left_line": 646, + "right_line": 652 + }, + { + "left_line": 647, + "right_line": 653 + }, + { + "left_line": 648, + "right_line": 654 + }, + { + "left_line": 649, + "right_line": 655 + }, + { + "left_line": 650, + "right_line": 656 + }, + { + "left_line": 651, + "right_line": 657 + }, + { + "left_line": 652, + "right_line": 658 + }, + { + "left_line": 653, + "right_line": 659 + }, + { + "left_line": 654, + "right_line": 660 + }, + { + "left_line": 655, + "right_line": 661 + }, + { + "left_line": 656, + "right_line": 662 + }, + { + "left_line": 657, + "right_line": 663 + }, + { + "left_line": 658, + "right_line": 664 + }, + { + "left_line": 659, + "right_line": 665 + }, + { + "left_line": 660, + "right_line": 666 + }, + { + "left_line": 661, + "right_line": 667 + }, + { + "left_line": 662, + "right_line": 668 + }, + { + "left_line": 663, + "right_line": 669 + }, + { + "left_line": 664, + "right_line": 670 + }, + { + "left_line": 665, + "right_line": 671 + }, + { + "left_line": 666, + "right_line": 672 + }, + { + "left_line": 667, + "right_line": 673 + }, + { + "left_line": 668, + "right_line": 674 + }, + { + "left_line": 669, + "right_line": 675 + }, + { + "left_line": 670, + "right_line": 676 + }, + { + "left_line": 671, + "right_line": 677 + }, + { + "left_line": 672, + "right_line": 678 + }, + { + "left_line": 673, + "right_line": 679 + }, + { + "left_line": 674, + "right_line": 680 + }, + { + "left_line": 675, + "right_line": 681 + }, + { + "left_line": 676, + "right_line": 682 + }, + { + "left_line": 677, + "right_line": 683 + }, + { + "left_line": 678, + "right_line": 684 + }, + { + "left_line": 679, + "right_line": 685 + }, + { + "left_line": 680, + "right_line": 686 + }, + { + "left_line": 681, + "right_line": 687 + }, + { + "left_line": 682, + "right_line": 688 + }, + { + "left_line": 683, + "right_line": 689 + }, + { + "left_line": 684, + "right_line": 690 + }, + { + "left_line": 685, + "right_line": 691 + }, + { + "left_line": 686, + "right_line": 692 + }, + { + "left_line": 687, + "right_line": 693 + }, + { + "left_line": 688, + "right_line": 694 + }, + { + "left_line": 689, + "right_line": 695 + }, + { + "left_line": 690, + "right_line": 696 + }, + { + "left_line": 691, + "right_line": 697 + }, + { + "left_line": 692, + "right_line": 698 + }, + { + "left_line": 693, + "right_line": 699 + }, + { + "left_line": 694, + "right_line": 700 + }, + { + "left_line": 695, + "right_line": 701 + }, + { + "left_line": 696, + "right_line": 702 + }, + { + "left_line": 697, + "right_line": 703 + }, + { + "left_line": 698, + "right_line": 704 + }, + { + "left_line": 699, + "right_line": 705 + }, + { + "left_line": 700, + "right_line": 706 + }, + { + "left_line": 701, + "right_line": 707 + }, + { + "left_line": 702, + "right_line": 708 + }, + { + "left_line": 703, + "right_line": 709 + }, + { + "left_line": 704, + "right_line": 710 + }, + { + "left_line": 705, + "right_line": 711 + }, + { + "left_line": 706, + "right_line": 712 + }, + { + "left_line": 707, + "right_line": 713 + }, + { + "left_line": 708, + "right_line": 714 + }, + { + "left_line": 709, + "right_line": 715 + }, + { + "left_line": 710, + "right_line": 716 + }, + { + "left_line": 711, + "right_line": 717 + }, + { + "left_line": 712, + "right_line": 718 + }, + { + "left_line": 713, + "right_line": 719 + }, + { + "left_line": 714, + "right_line": 720 + }, + { + "left_line": 715, + "right_line": 721 + }, + { + "left_line": 716, + "right_line": 722 + }, + { + "left_line": 717, + "right_line": 723 + }, + { + "left_line": 718, + "right_line": 724 + }, + { + "left_line": 719, + "right_line": 725 + }, + { + "left_line": 720, + "right_line": 726 + }, + { + "left_line": 721, + "right_line": 727 + }, + { + "left_line": 722, + "right_line": 728 + }, + { + "left_line": 723, + "right_line": 729 + }, + { + "left_line": 724, + "right_line": 730 + }, + { + "left_line": 725, + "right_line": 731 + }, + { + "left_line": 726, + "right_line": 732 + }, + { + "left_line": 727, + "right_line": 733 + }, + { + "left_line": 728, + "right_line": 734 + }, + { + "left_line": 729, + "right_line": 735 + }, + { + "left_line": 730, + "right_line": 736 + }, + { + "left_line": 731, + "right_line": 737 + }, + { + "left_line": 732, + "right_line": 738 + }, + { + "left_line": 733, + "right_line": 739 + }, + { + "left_line": 734, + "right_line": 740 + }, + { + "left_line": 735, + "right_line": 741 + }, + { + "left_line": 736, + "right_line": 742 + }, + { + "left_line": 737, + "right_line": 743 + }, + { + "left_line": 738, + "right_line": 744 + }, + { + "left_line": 739, + "right_line": 745 + }, + { + "left_line": 740, + "right_line": 746 + }, + { + "left_line": 741, + "right_line": 747 + }, + { + "left_line": 742, + "right_line": 748 + }, + { + "left_line": 743, + "right_line": 749 + }, + { + "left_line": 744, + "right_line": 750 + }, + { + "left_line": 745, + "right_line": 751 + }, + { + "left_line": 746, + "right_line": 752 + }, + { + "left_line": 747, + "right_line": 753 + }, + { + "left_line": 748, + "right_line": 754 + }, + { + "left_line": 749, + "right_line": 755 + }, + { + "left_line": 750, + "right_line": 756 + }, + { + "left_line": 751, + "right_line": 757 + }, + { + "left_line": 752, + "right_line": 758 + }, + { + "left_line": 753, + "right_line": 759 + }, + { + "left_line": 754, + "right_line": 760 + }, + { + "left_line": 755, + "right_line": 761 + }, + { + "left_line": 756, + "right_line": 762 + }, + { + "left_line": 757, + "right_line": 763 + }, + { + "left_line": 758, + "right_line": 764 + }, + { + "left_line": 759, + "right_line": 765 + }, + { + "left_line": 760, + "right_line": 766 + }, + { + "left_line": 761, + "right_line": 767 + }, + { + "left_line": 762, + "right_line": 768 + }, + { + "left_line": 763, + "right_line": 769 + }, + { + "left_line": 764, + "right_line": 770 + }, + { + "left_line": 765, + "right_line": 771 + }, + { + "left_line": 766, + "right_line": 772 + }, + { + "left_line": 767, + "right_line": 773 + }, + { + "left_line": 768, + "right_line": 774 + }, + { + "left_line": 769, + "right_line": 775 + }, + { + "left_line": 770, + "right_line": 776 + }, + { + "left_line": 771, + "right_line": 777 + }, + { + "left_line": 772, + "right_line": 778 + }, + { + "left_line": 773, + "right_line": 779 + }, + { + "left_line": 774, + "right_line": 780 + }, + { + "left_line": 775, + "right_line": 781 + }, + { + "left_line": 776, + "right_line": 782 + }, + { + "left_line": 777, + "right_line": 783 + }, + { + "left_line": 778, + "right_line": 784 + }, + { + "left_line": 779, + "right_line": 785 + }, + { + "left_line": 780, + "right_line": 786 + }, + { + "left_line": 781, + "right_line": 787 + }, + { + "left_line": 782, + "right_line": 788 + }, + { + "left_line": 783, + "right_line": 789 + }, + { + "left_line": 784, + "right_line": 790 + }, + { + "left_line": 785, + "right_line": 791 + }, + { + "left_line": 786, + "right_line": 792 + }, + { + "left_line": 787, + "right_line": 793 + }, + { + "left_line": 788, + "right_line": 794 + }, + { + "left_line": 789, + "right_line": 795 + }, + { + "left_line": 790, + "right_line": 796 + }, + { + "left_line": 791, + "right_line": 797 + }, + { + "left_line": 792, + "right_line": 798 + }, + { + "left_line": 793, + "right_line": 799 + }, + { + "left_line": 794, + "right_line": 800 + }, + { + "left_line": 795, + "right_line": 801 + }, + { + "left_line": 796, + "right_line": 802 + }, + { + "left_line": 797, + "right_line": 803 + }, + { + "left_line": 798, + "right_line": 804 + }, + { + "left_line": 799, + "right_line": 805 + }, + { + "left_line": 800, + "right_line": 806 + }, + { + "left_line": 801, + "right_line": 807 + }, + { + "left_line": 802, + "right_line": 808 + }, + { + "left_line": 803, + "right_line": 809 + }, + { + "left_line": 804, + "right_line": 810 + }, + { + "left_line": 805, + "right_line": 811 + }, + { + "left_line": 806, + "right_line": 812 + }, + { + "left_line": 807, + "right_line": 813 + }, + { + "left_line": 808, + "right_line": 814 + }, + { + "left_line": 809, + "right_line": 815 + }, + { + "left_line": 810, + "right_line": 816 + }, + { + "left_line": 811, + "right_line": 817 + }, + { + "left_line": 812, + "right_line": 818 + }, + { + "left_line": 813, + "right_line": 819 + }, + { + "left_line": 814, + "right_line": 820 + }, + { + "left_line": 815, + "right_line": 821 + }, + { + "left_line": 816, + "right_line": 822 + }, + { + "left_line": 817, + "right_line": 823 + }, + { + "left_line": 818, + "right_line": 824 + }, + { + "left_line": 819, + "right_line": 825 + }, + { + "left_line": 820, + "right_line": 826 + }, + { + "left_line": 821, + "right_line": 827 + }, + { + "left_line": 822, + "right_line": 828 + }, + { + "left_line": 823, + "right_line": 829 + }, + { + "left_line": 824, + "right_line": 830 + }, + { + "left_line": 825, + "right_line": 831 + }, + { + "left_line": 826, + "right_line": 832 + }, + { + "left_line": 827, + "right_line": 833 + }, + { + "left_line": 828, + "right_line": 834 + }, + { + "left_line": 829, + "right_line": 835 + }, + { + "left_line": 830, + "right_line": 836 + }, + { + "left_line": 831, + "right_line": 837 + }, + { + "left_line": 832, + "right_line": 838 + }, + { + "left_line": 833, + "right_line": 839 + }, + { + "left_line": 834, + "right_line": 840 + }, + { + "left_line": 835, + "right_line": 841 + }, + { + "left_line": 836, + "right_line": 842 + }, + { + "left_line": 837, + "right_line": 843 + }, + { + "left_line": 838, + "right_line": 844 + }, + { + "left_line": 839, + "right_line": 845 + }, + { + "left_line": 840, + "right_line": 846 + }, + { + "left_line": 841, + "right_line": 847 + }, + { + "left_line": 842, + "right_line": 848 + }, + { + "left_line": 843, + "right_line": 849 + }, + { + "left_line": 844, + "right_line": 850 + }, + { + "left_line": 845, + "right_line": 851 + }, + { + "left_line": 846, + "right_line": 852 + }, + { + "left_line": 847, + "right_line": 853 + }, + { + "left_line": 848, + "right_line": 854 + }, + { + "left_line": 849, + "right_line": 855 + }, + { + "left_line": 850, + "right_line": 856 + }, + { + "left_line": 851, + "right_line": 857 + }, + { + "left_line": 852, + "right_line": 858 + }, + { + "left_line": 853, + "right_line": 859 + }, + { + "left_line": 854, + "right_line": 860 + }, + { + "left_line": 855, + "right_line": 861 + }, + { + "left_line": 856, + "right_line": 862 + }, + { + "left_line": 857, + "right_line": 863 + }, + { + "left_line": 858, + "right_line": 864 + }, + { + "left_line": 859, + "right_line": 865 + }, + { + "left_line": 860, + "right_line": 866 + }, + { + "left_line": 861, + "right_line": 867 + }, + { + "left_line": 862, + "right_line": 868 + }, + { + "left_line": 863, + "right_line": 869 + }, + { + "left_line": 864, + "right_line": 870 + }, + { + "left_line": 865, + "right_line": 871 + }, + { + "left_line": 866, + "right_line": 872 + }, + { + "left_line": 867, + "right_line": 873 + }, + { + "left_line": 868, + "right_line": 874 + }, + { + "left_line": 869, + "right_line": 875 + }, + { + "left_line": 870, + "right_line": 876 + }, + { + "left_line": 871, + "right_line": 877 + }, + { + "left_line": 872, + "right_line": 878 + }, + { + "left_line": 873, + "right_line": 879 + }, + { + "left_line": 874, + "right_line": 880 + }, + { + "left_line": 875, + "right_line": 881 + }, + { + "left_line": 876, + "right_line": 882 + }, + { + "left_line": 877, + "right_line": 883 + }, + { + "left_line": 878, + "right_line": 884 + }, + { + "left_line": 879, + "right_line": 885 + }, + { + "left_line": 880, + "right_line": 886 + }, + { + "left_line": 881, + "right_line": 887 + }, + { + "left_line": 882, + "right_line": 888 + }, + { + "left_line": 883, + "right_line": 889 + }, + { + "left_line": 884, + "right_line": 890 + }, + { + "left_line": 885, + "right_line": 891 + }, + { + "left_line": 886, + "right_line": 892 + }, + { + "left_line": 887, + "right_line": 893 + }, + { + "left_line": 888, + "right_line": 894 + }, + { + "left_line": 889, + "right_line": 895 + }, + { + "left_line": 890, + "right_line": 896 + }, + { + "left_line": 891, + "right_line": 897 + }, + { + "left_line": 892, + "right_line": 898 + }, + { + "left_line": 893, + "right_line": 899 + }, + { + "left_line": 894, + "right_line": 900 + }, + { + "left_line": 895, + "right_line": 901 + }, + { + "left_line": 896, + "right_line": 902 + }, + { + "left_line": 897, + "right_line": 903 + }, + { + "left_line": 898, + "right_line": 904 + }, + { + "left_line": 899, + "right_line": 905 + }, + { + "left_line": 900, + "right_line": 906 + }, + { + "left_line": 901, + "right_line": 907 + }, + { + "left_line": 902, + "right_line": 908 + }, + { + "left_line": 903, + "right_line": 909 + }, + { + "left_line": 904, + "right_line": 910 + }, + { + "left_line": 905, + "right_line": 911 + }, + { + "left_line": 906, + "right_line": 912 + }, + { + "left_line": 907, + "right_line": 913 + }, + { + "left_line": 908, + "right_line": 914 + }, + { + "left_line": 909, + "right_line": 915 + }, + { + "left_line": 910, + "right_line": 916 + }, + { + "left_line": 911, + "right_line": 917 + }, + { + "left_line": 912, + "right_line": 918 + }, + { + "left_line": 913, + "right_line": 919 + }, + { + "left_line": 914, + "right_line": 920 + }, + { + "left_line": 915, + "right_line": 921 + }, + { + "left_line": 916, + "right_line": 922 + }, + { + "left_line": 917, + "right_line": 923 + }, + { + "left_line": 918, + "right_line": 924 + }, + { + "left_line": 919, + "right_line": 925 + }, + { + "left_line": 920, + "right_line": 926 + }, + { + "left_line": 921, + "right_line": 927 + }, + { + "left_line": 922, + "right_line": 928 + }, + { + "left_line": 923, + "right_line": 929 + }, + { + "left_line": 924, + "right_line": 930 + }, + { + "left_line": 925, + "right_line": 931 + }, + { + "left_line": 926, + "right_line": 932 + }, + { + "left_line": 927, + "right_line": 933 + }, + { + "left_line": 928, + "right_line": 934 + }, + { + "left_line": 929, + "right_line": 935 + }, + { + "left_line": 930, + "right_line": 936 + }, + { + "left_line": 931, + "right_line": 937 + }, + { + "left_line": 932, + "right_line": 938 + }, + { + "left_line": 933, + "right_line": 939 + }, + { + "left_line": 934, + "right_line": 940 + }, + { + "left_line": 935, + "right_line": 941 + }, + { + "left_line": 936, + "right_line": 942 + }, + { + "left_line": 937, + "right_line": 943 + }, + { + "left_line": 938, + "right_line": 944 + }, + { + "left_line": 939, + "right_line": 945 + }, + { + "left_line": 940, + "right_line": 946 + }, + { + "left_line": 941, + "right_line": 947 + }, + { + "left_line": 942, + "right_line": 948 + }, + { + "left_line": 943, + "right_line": 949 + }, + { + "left_line": 944, + "right_line": 950 + }, + { + "left_line": 945, + "right_line": 951 + }, + { + "left_line": 946, + "right_line": 952 + }, + { + "left_line": 947, + "right_line": 953 + }, + { + "left_line": 948, + "right_line": 954 + }, + { + "left_line": 949, + "right_line": 955 + }, + { + "left_line": 950, + "right_line": 956 + }, + { + "left_line": 951, + "right_line": 957 + }, + { + "left_line": 952, + "right_line": 958 + }, + { + "left_line": 953, + "right_line": 959 + }, + { + "left_line": 954, + "right_line": 960 + }, + { + "left_line": 955, + "right_line": 961 + }, + { + "left_line": 956, + "right_line": 962 + }, + { + "left_line": 957, + "right_line": 963 + }, + { + "left_line": 958, + "right_line": 964 + }, + { + "left_line": 959, + "right_line": 965 + }, + { + "left_line": 960, + "right_line": 966 + }, + { + "left_line": 961, + "right_line": 967 + }, + { + "left_line": 962, + "right_line": 968 + }, + { + "left_line": 963, + "right_line": 969 + }, + { + "left_line": 964, + "right_line": 970 + }, + { + "left_line": 965, + "right_line": 971 + }, + { + "left_line": 966, + "right_line": 972 + }, + { + "left_line": 967, + "right_line": 973 + }, + { + "left_line": 968, + "right_line": 974 + }, + { + "left_line": 969, + "right_line": 975 + }, + { + "left_line": 970, + "right_line": 976 + }, + { + "left_line": 971, + "right_line": 977 + }, + { + "left_line": 972, + "right_line": 978 + }, + { + "left_line": 973, + "right_line": 979 + }, + { + "left_line": 974, + "right_line": 980 + }, + { + "left_line": 975, + "right_line": 981 + }, + { + "left_line": 976, + "right_line": 982 + }, + { + "left_line": 977, + "right_line": 983 + }, + { + "left_line": 978, + "right_line": 984 + }, + { + "left_line": 979, + "right_line": 985 + }, + { + "left_line": 980, + "right_line": 986 + }, + { + "left_line": 981, + "right_line": 987 + }, + { + "left_line": 982, + "right_line": 988 + }, + { + "left_line": 983, + "right_line": 989 + }, + { + "left_line": 984, + "right_line": 990 + }, + { + "left_line": 985, + "right_line": 991 + }, + { + "left_line": 986, + "right_line": 992 + }, + { + "left_line": 987, + "right_line": 993 + }, + { + "left_line": 988, + "right_line": 994 + }, + { + "left_line": 989, + "right_line": 995 + }, + { + "left_line": 990, + "right_line": 996 + }, + { + "left_line": 991, + "right_line": 997 + }, + { + "left_line": 992, + "right_line": 998 + }, + { + "left_line": 993, + "right_line": 999 + }, + { + "left_line": 994, + "right_line": 1000 + }, + { + "left_line": 995, + "right_line": 1001 + }, + { + "left_line": 996, + "right_line": 1002 + }, + { + "left_line": 997, + "right_line": 1003 + }, + { + "left_line": 998, + "right_line": 1004 + }, + { + "left_line": 999, + "right_line": 1005 + }, + { + "left_line": 1000, + "right_line": 1006 + }, + { + "left_line": 1001, + "right_line": 1007 + }, + { + "left_line": 1002, + "right_line": 1008 + }, + { + "left_line": 1003, + "right_line": 1009 + }, + { + "left_line": 1004, + "right_line": 1010 + }, + { + "left_line": 1005, + "right_line": 1011 + }, + { + "left_line": 1006, + "right_line": 1012 + }, + { + "left_line": 1007, + "right_line": 1013 + }, + { + "left_line": 1008, + "right_line": 1014 + }, + { + "left_line": 1009, + "right_line": 1015 + }, + { + "left_line": 1010, + "right_line": 1016 + }, + { + "left_line": 1011, + "right_line": 1017 + }, + { + "left_line": 1012, + "right_line": 1018 + }, + { + "left_line": 1013, + "right_line": 1019 + }, + { + "left_line": 1014, + "right_line": 1020 + }, + { + "left_line": 1015, + "right_line": 1021 + }, + { + "left_line": 1016, + "right_line": 1022 + }, + { + "left_line": 1017, + "right_line": 1023 + }, + { + "left_line": 1018, + "right_line": 1024 + }, + { + "left_line": 1019, + "right_line": 1025 + }, + { + "left_line": 1020, + "right_line": 1026 + }, + { + "left_line": 1021, + "right_line": 1027 + }, + { + "left_line": 1022, + "right_line": 1028 + }, + { + "left_line": 1023, + "right_line": 1029 + }, + { + "left_line": 1024, + "right_line": 1030 + }, + { + "left_line": 1025, + "right_line": 1031 + }, + { + "left_line": 1026, + "right_line": 1032 + }, + { + "left_line": 1027, + "right_line": 1033 + }, + { + "left_line": 1028, + "right_line": 1034 + }, + { + "left_line": 1029, + "right_line": 1035 + }, + { + "left_line": 1030, + "right_line": 1036 + }, + { + "left_line": 1031, + "right_line": 1037 + }, + { + "left_line": 1032, + "right_line": 1038 + }, + { + "left_line": 1033, + "right_line": 1039 + }, + { + "left_line": 1034, + "right_line": 1040 + }, + { + "left_line": 1035, + "right_line": 1041 + }, + { + "left_line": 1036, + "right_line": 1042 + }, + { + "left_line": 1037, + "right_line": 1043 + }, + { + "left_line": 1038, + "right_line": 1044 + }, + { + "left_line": 1039, + "right_line": 1045 + }, + { + "left_line": 1040, + "right_line": 1046 + }, + { + "left_line": 1041, + "right_line": 1047 + }, + { + "left_line": 1042, + "right_line": 1048 + }, + { + "left_line": 1043, + "right_line": 1049 + }, + { + "left_line": 1044, + "right_line": 1050 + }, + { + "left_line": 1045, + "right_line": 1051 + }, + { + "left_line": 1046, + "right_line": 1052 + }, + { + "left_line": 1047, + "right_line": 1053 + }, + { + "left_line": 1048, + "right_line": 1054 + }, + { + "left_line": 1049, + "right_line": 1055 + }, + { + "left_line": 1050, + "right_line": 1056 + }, + { + "left_line": 1051, + "right_line": 1057 + }, + { + "left_line": 1052, + "right_line": 1058 + }, + { + "left_line": 1053, + "right_line": 1059 + }, + { + "left_line": 1054, + "right_line": 1060 + }, + { + "left_line": 1055, + "right_line": 1061 + }, + { + "left_line": 1056, + "right_line": 1062 + }, + { + "left_line": 1057, + "right_line": 1063 + }, + { + "left_line": 1058, + "right_line": 1064 + }, + { + "left_line": 1059, + "right_line": 1065 + }, + { + "left_line": 1060, + "right_line": 1066 + }, + { + "left_line": 1061, + "right_line": 1067 + }, + { + "left_line": 1062, + "right_line": 1068 + }, + { + "left_line": 1063, + "right_line": 1069 + }, + { + "left_line": 1064, + "right_line": 1070 + }, + { + "left_line": 1065, + "right_line": 1071 + }, + { + "left_line": 1066, + "right_line": 1072 + }, + { + "left_line": 1067, + "right_line": 1073 + }, + { + "left_line": 1068, + "right_line": 1074 + }, + { + "left_line": 1069, + "right_line": 1075 + }, + { + "left_line": 1070, + "right_line": 1076 + }, + { + "left_line": 1071, + "right_line": 1077 + }, + { + "left_line": 1072, + "right_line": 1078 + }, + { + "left_line": 1073, + "right_line": 1079 + }, + { + "left_line": 1074, + "right_line": 1080 + }, + { + "left_line": 1075, + "right_line": 1081 + }, + { + "left_line": 1076, + "right_line": 1082 + }, + { + "left_line": 1077, + "right_line": 1083 + }, + { + "left_line": 1078, + "right_line": 1084 + }, + { + "left_line": 1079, + "right_line": 1085 + }, + { + "left_line": 1080, + "right_line": 1086 + }, + { + "left_line": 1081, + "right_line": 1087 + }, + { + "left_line": 1082, + "right_line": 1088 + }, + { + "left_line": 1083, + "right_line": 1089 + }, + { + "left_line": 1084, + "right_line": 1090 + }, + { + "left_line": 1085, + "right_line": 1091 + }, + { + "left_line": 1086, + "right_line": 1092 + }, + { + "left_line": 1087, + "right_line": 1093 + }, + { + "left_line": 1088, + "right_line": 1094 + }, + { + "left_line": 1089, + "right_line": 1095 + }, + { + "left_line": 1090, + "right_line": 1096 + }, + { + "left_line": 1091, + "right_line": 1097 + }, + { + "left_line": 1092, + "right_line": 1098 + }, + { + "left_line": 1093, + "right_line": 1099 + }, + { + "left_line": 1094, + "right_line": 1100 + }, + { + "left_line": 1095, + "right_line": 1101 + }, + { + "left_line": 1096, + "right_line": 1102 + }, + { + "left_line": 1097, + "right_line": 1103 + }, + { + "left_line": 1098, + "right_line": 1104 + }, + { + "left_line": 1099, + "right_line": 1105 + }, + { + "left_line": 1100, + "right_line": 1106 + }, + { + "left_line": 1101, + "right_line": 1107 + }, + { + "left_line": 1102, + "right_line": 1108 + }, + { + "left_line": 1103, + "right_line": 1109 + }, + { + "left_line": 1104, + "right_line": 1110 + }, + { + "left_line": 1105, + "right_line": 1111 + }, + { + "left_line": 1106, + "right_line": 1112 + }, + { + "left_line": 1107, + "right_line": 1113 + }, + { + "left_line": 1108, + "right_line": 1114 + }, + { + "left_line": 1109, + "right_line": 1115 + }, + { + "left_line": 1110, + "right_line": 1116 + }, + { + "left_line": 1111, + "right_line": 1117 + }, + { + "left_line": 1112, + "right_line": 1118 + }, + { + "left_line": 1113, + "right_line": 1119 + }, + { + "left_line": 1114, + "right_line": 1120 + }, + { + "left_line": 1115, + "right_line": 1121 + }, + { + "left_line": 1116, + "right_line": 1122 + }, + { + "left_line": 1117, + "right_line": 1123 + }, + { + "left_line": 1118, + "right_line": 1124 + }, + { + "left_line": 1119, + "right_line": 1125 + }, + { + "left_line": 1120, + "right_line": 1126 + }, + { + "left_line": 1121, + "right_line": 1127 + }, + { + "left_line": 1122, + "right_line": 1128 + }, + { + "left_line": 1123, + "right_line": 1129 + }, + { + "left_line": 1124, + "right_line": 1130 + }, + { + "left_line": 1125, + "right_line": 1131 + }, + { + "left_line": 1126, + "right_line": 1132 + }, + { + "left_line": 1127, + "right_line": 1133 + }, + { + "left_line": 1128, + "right_line": 1134 + }, + { + "left_line": 1129, + "right_line": 1135 + }, + { + "left_line": 1130, + "right_line": 1136 + }, + { + "left_line": 1131, + "right_line": 1137 + }, + { + "left_line": 1132, + "right_line": 1138 + }, + { + "left_line": 1133, + "right_line": 1139 + }, + { + "left_line": 1134, + "right_line": 1140 + }, + { + "left_line": 1135, + "right_line": 1141 + }, + { + "left_line": 1136, + "right_line": 1142 + }, + { + "left_line": 1137, + "right_line": 1143 + }, + { + "left_line": 1138, + "right_line": 1144 + }, + { + "left_line": 1139, + "right_line": 1145 + }, + { + "left_line": 1140, + "right_line": 1146 + }, + { + "left_line": 1141, + "right_line": 1147 + }, + { + "left_line": 1142, + "right_line": 1148 + }, + { + "left_line": 1143, + "right_line": 1149 + }, + { + "left_line": 1144, + "right_line": 1150 + }, + { + "left_line": 1145, + "right_line": 1151 + }, + { + "left_line": 1146, + "right_line": 1152 + }, + { + "left_line": 1147, + "right_line": 1153 + }, + { + "left_line": 1148, + "right_line": 1154 + }, + { + "left_line": 1149, + "right_line": 1155 + }, + { + "left_line": 1150, + "right_line": 1156 + }, + { + "left_line": 1151, + "right_line": 1157 + }, + { + "left_line": 1152, + "right_line": 1158 + }, + { + "left_line": 1153, + "right_line": 1159 + }, + { + "left_line": 1154, + "right_line": 1160 + }, + { + "left_line": 1155, + "right_line": 1161 + }, + { + "left_line": 1156, + "right_line": 1162 + }, + { + "left_line": 1157, + "right_line": 1163 + }, + { + "left_line": 1158, + "right_line": 1164 + }, + { + "left_line": 1159, + "right_line": 1165 + }, + { + "left_line": 1160, + "right_line": 1166 + }, + { + "left_line": 1161, + "right_line": 1167 + }, + { + "left_line": 1162, + "right_line": 1168 + }, + { + "left_line": 1163, + "right_line": 1169 + }, + { + "left_line": 1164, + "right_line": 1170 + }, + { + "left_line": 1165, + "right_line": 1171 + }, + { + "left_line": 1166, + "right_line": 1172 + }, + { + "left_line": 1167, + "right_line": 1173 + }, + { + "left_line": 1168, + "right_line": 1174 + }, + { + "left_line": 1169, + "right_line": 1175 + }, + { + "left_line": 1170, + "right_line": 1176 + }, + { + "left_line": 1171, + "right_line": 1177 + }, + { + "left_line": 1172, + "right_line": 1178 + }, + { + "left_line": 1173, + "right_line": 1179 + }, + { + "left_line": 1174, + "right_line": 1180 + }, + { + "left_line": 1175, + "right_line": 1181 + }, + { + "left_line": 1176, + "right_line": 1182 + }, + { + "left_line": 1177, + "right_line": 1183 + }, + { + "left_line": 1178, + "right_line": 1184 + }, + { + "left_line": 1179, + "right_line": 1185 + }, + { + "left_line": 1180, + "right_line": 1186 + }, + { + "left_line": 1181, + "right_line": 1187 + }, + { + "left_line": 1182, + "right_line": 1188 + }, + { + "left_line": 1183, + "right_line": 1189 + }, + { + "left_line": 1184, + "right_line": 1190 + }, + { + "left_line": 1185, + "right_line": 1191 + }, + { + "left_line": 1186, + "right_line": 1192 + }, + { + "left_line": 1187, + "right_line": 1193 + }, + { + "left_line": 1188, + "right_line": 1194 + }, + { + "left_line": 1189, + "right_line": 1195 + }, + { + "left_line": 1190, + "right_line": 1196 + }, + { + "left_line": 1191, + "right_line": 1197 + }, + { + "left_line": 1192, + "right_line": 1198 + }, + { + "left_line": 1193, + "right_line": 1199 + }, + { + "left_line": 1194, + "right_line": 1200 + }, + { + "left_line": 1195, + "right_line": 1201 + }, + { + "left_line": 1196, + "right_line": 1202 + }, + { + "left_line": 1197, + "right_line": 1203 + }, + { + "left_line": 1198, + "right_line": 1204 + }, + { + "left_line": 1199, + "right_line": 1205 + }, + { + "left_line": 1200, + "right_line": 1206 + }, + { + "left_line": 1201, + "right_line": 1207 + }, + { + "left_line": 1202, + "right_line": 1208 + }, + { + "left_line": 1203, + "right_line": 1209 + }, + { + "left_line": 1204, + "right_line": 1210 + }, + { + "left_line": 1205, + "right_line": 1211 + }, + { + "left_line": 1206, + "right_line": 1212 + }, + { + "left_line": 1207, + "right_line": 1213 + }, + { + "left_line": 1208, + "right_line": 1214 + }, + { + "left_line": 1209, + "right_line": 1215 + }, + { + "left_line": 1210, + "right_line": 1216 + }, + { + "left_line": 1211, + "right_line": 1217 + }, + { + "left_line": 1212, + "right_line": 1218 + }, + { + "left_line": 1213, + "right_line": 1219 + }, + { + "left_line": 1214, + "right_line": 1220 + }, + { + "left_line": 1215, + "right_line": 1221 + }, + { + "left_line": 1216, + "right_line": 1222 + }, + { + "left_line": 1217, + "right_line": 1223 + }, + { + "left_line": 1218, + "right_line": 1224 + }, + { + "left_line": 1219, + "right_line": 1225 + }, + { + "left_line": 1220, + "right_line": 1226 + }, + { + "left_line": 1221, + "right_line": 1227 + }, + { + "left_line": 1222, + "right_line": 1228 + }, + { + "left_line": 1223, + "right_line": 1229 + }, + { + "left_line": 1224, + "right_line": 1230 + }, + { + "left_line": 1225, + "right_line": 1231 + }, + { + "left_line": 1226, + "right_line": 1232 + }, + { + "left_line": 1227, + "right_line": 1233 + }, + { + "left_line": 1228, + "right_line": 1234 + }, + { + "left_line": 1229, + "right_line": 1235 + }, + { + "left_line": 1230, + "right_line": 1236 + }, + { + "left_line": 1231, + "right_line": 1237 + }, + { + "left_line": 1232, + "right_line": 1238 + }, + { + "left_line": 1233, + "right_line": 1239 + }, + { + "left_line": 1234, + "right_line": 1240 + }, + { + "left_line": 1235, + "right_line": 1241 + }, + { + "left_line": 1236, + "right_line": 1242 + }, + { + "left_line": 1237, + "right_line": 1243 + }, + { + "left_line": 1238, + "right_line": 1244 + }, + { + "left_line": 1239, + "right_line": 1245 + }, + { + "left_line": 1240, + "right_line": 1246 + }, + { + "left_line": 1241, + "right_line": 1247 + }, + { + "left_line": 1242, + "right_line": 1248 + }, + { + "left_line": 1243, + "right_line": 1249 + }, + { + "left_line": 1244, + "right_line": 1250 + }, + { + "left_line": 1245, + "right_line": 1251 + }, + { + "left_line": 1246, + "right_line": 1252 + }, + { + "left_line": 1247, + "right_line": 1253 + }, + { + "left_line": 1248, + "right_line": 1254 + }, + { + "left_line": 1249, + "right_line": 1255 + }, + { + "left_line": -1, + "right_line": 1256 + }, + { + "left_line": -1, + "right_line": 1257 + }, + { + "left_line": -1, + "right_line": 1258 + }, + { + "left_line": -1, + "right_line": 1259 + }, + { + "left_line": -1, + "right_line": 1260 + }, + { + "left_line": -1, + "right_line": 1261 + }, + { + "left_line": -1, + "right_line": 1262 + }, + { + "left_line": -1, + "right_line": 1263 + }, + { + "left_line": -1, + "right_line": 1264 + }, + { + "left_line": -1, + "right_line": 1265 + }, + { + "left_line": -1, + "right_line": 1266 + }, + { + "left_line": -1, + "right_line": 1267 + }, + { + "left_line": -1, + "right_line": 1268 + }, + { + "left_line": -1, + "right_line": 1269 + }, + { + "left_line": -1, + "right_line": 1270 + }, + { + "left_line": -1, + "right_line": 1271 + }, + { + "left_line": -1, + "right_line": 1272 + }, + { + "left_line": 1250, + "right_line": 1273 + }, + { + "left_line": 1251, + "right_line": 1274 + }, + { + "left_line": 1252, + "right_line": 1275 + }, + { + "left_line": 1253, + "right_line": 1276 + }, + { + "left_line": 1254, + "right_line": 1277 + }, + { + "left_line": 1255, + "right_line": 1278 + }, + { + "left_line": 1256, + "right_line": 1279 + }, + { + "left_line": 1257, + "right_line": 1280 + }, + { + "left_line": 1258, + "right_line": 1281 + }, + { + "left_line": 1259, + "right_line": 1282 + }, + { + "left_line": 1260, + "right_line": 1283 + }, + { + "left_line": 1261, + "right_line": 1284 + }, + { + "left_line": 1262, + "right_line": 1285 + }, + { + "left_line": 1263, + "right_line": 1286 + }, + { + "left_line": 1264, + "right_line": 1287 + }, + { + "left_line": 1265, + "right_line": 1288 + }, + { + "left_line": 1266, + "right_line": 1289 + }, + { + "left_line": 1267, + "right_line": 1290 + }, + { + "left_line": 1268, + "right_line": 1291 + }, + { + "left_line": 1269, + "right_line": 1292 + }, + { + "left_line": 1270, + "right_line": 1293 + }, + { + "left_line": 1271, + "right_line": 1294 + }, + { + "left_line": 1272, + "right_line": 1295 + }, + { + "left_line": 1273, + "right_line": 1296 + }, + { + "left_line": 1274, + "right_line": 1297 + }, + { + "left_line": 1275, + "right_line": 1298 + }, + { + "left_line": 1276, + "right_line": 1299 + }, + { + "left_line": 1277, + "right_line": 1300 + }, + { + "left_line": 1278, + "right_line": 1301 + }, + { + "left_line": 1279, + "right_line": 1302 + }, + { + "left_line": 1280, + "right_line": 1303 + }, + { + "left_line": 1281, + "right_line": 1304 + }, + { + "left_line": 1282, + "right_line": 1305 + }, + { + "left_line": 1283, + "right_line": 1306 + }, + { + "left_line": 1284, + "right_line": 1307 + }, + { + "left_line": 1285, + "right_line": 1308 + }, + { + "left_line": 1286, + "right_line": 1309 + }, + { + "left_line": 1287, + "right_line": 1310 + }, + { + "left_line": 1288, + "right_line": 1311 + }, + { + "left_line": 1289, + "right_line": 1312 + }, + { + "left_line": 1290, + "right_line": 1313 + }, + { + "left_line": 1291, + "right_line": 1314 + }, + { + "left_line": 1292, + "right_line": 1315 + }, + { + "left_line": 1293, + "right_line": 1316 + }, + { + "left_line": 1294, + "right_line": 1317 + }, + { + "left_line": 1295, + "right_line": 1318 + }, + { + "left_line": 1296, + "right_line": 1319 + }, + { + "left_line": 1297, + "right_line": 1320 + }, + { + "left_line": 1298, + "right_line": 1321 + }, + { + "left_line": 1299, + "right_line": 1322 + }, + { + "left_line": 1300, + "right_line": 1323 + }, + { + "left_line": 1301, + "right_line": 1324 + }, + { + "left_line": 1302, + "right_line": 1325 + }, + { + "left_line": 1303, + "right_line": 1326 + }, + { + "left_line": 1304, + "right_line": 1327 + }, + { + "left_line": 1305, + "right_line": 1328 + }, + { + "left_line": 1306, + "right_line": 1329 + }, + { + "left_line": 1307, + "right_line": 1330 + }, + { + "left_line": 1308, + "right_line": 1331 + }, + { + "left_line": 1309, + "right_line": 1332 + }, + { + "left_line": 1310, + "right_line": 1333 + }, + { + "left_line": 1311, + "right_line": 1334 + }, + { + "left_line": 1312, + "right_line": 1335 + }, + { + "left_line": 1313, + "right_line": 1336 + }, + { + "left_line": 1314, + "right_line": 1337 + }, + { + "left_line": 1315, + "right_line": 1338 + }, + { + "left_line": 1316, + "right_line": 1339 + }, + { + "left_line": 1317, + "right_line": 1340 + }, + { + "left_line": 1318, + "right_line": 1341 + }, + { + "left_line": 1319, + "right_line": 1342 + }, + { + "left_line": 1320, + "right_line": 1343 + }, + { + "left_line": 1321, + "right_line": 1344 + }, + { + "left_line": 1322, + "right_line": 1345 + }, + { + "left_line": 1323, + "right_line": 1346 + }, + { + "left_line": 1324, + "right_line": 1347 + }, + { + "left_line": 1325, + "right_line": 1348 + }, + { + "left_line": 1326, + "right_line": 1349 + }, + { + "left_line": 1327, + "right_line": 1350 + }, + { + "left_line": 1328, + "right_line": 1351 + }, + { + "left_line": 1329, + "right_line": 1352 + }, + { + "left_line": 1330, + "right_line": 1353 + }, + { + "left_line": 1331, + "right_line": 1354 + }, + { + "left_line": 1332, + "right_line": 1355 + }, + { + "left_line": 1333, + "right_line": 1356 + }, + { + "left_line": 1334, + "right_line": 1357 + }, + { + "left_line": 1335, + "right_line": 1358 + }, + { + "left_line": 1336, + "right_line": 1359 + }, + { + "left_line": 1337, + "right_line": 1360 + }, + { + "left_line": 1338, + "right_line": 1361 + }, + { + "left_line": 1339, + "right_line": 1362 + }, + { + "left_line": 1340, + "right_line": 1363 + }, + { + "left_line": 1341, + "right_line": 1364 + }, + { + "left_line": 1342, + "right_line": 1365 + }, + { + "left_line": 1343, + "right_line": 1366 + }, + { + "left_line": 1344, + "right_line": 1367 + }, + { + "left_line": 1345, + "right_line": 1368 + }, + { + "left_line": 1346, + "right_line": 1369 + }, + { + "left_line": 1347, + "right_line": 1370 + }, + { + "left_line": 1348, + "right_line": 1371 + }, + { + "left_line": 1349, + "right_line": 1372 + }, + { + "left_line": 1350, + "right_line": 1373 + }, + { + "left_line": 1351, + "right_line": 1374 + }, + { + "left_line": 1352, + "right_line": 1375 + }, + { + "left_line": 1353, + "right_line": 1376 + }, + { + "left_line": 1354, + "right_line": 1377 + }, + { + "left_line": 1355, + "right_line": 1378 + }, + { + "left_line": 1356, + "right_line": 1379 + }, + { + "left_line": 1357, + "right_line": 1380 + }, + { + "left_line": 1358, + "right_line": 1381 + }, + { + "left_line": 1359, + "right_line": 1382 + }, + { + "left_line": 1360, + "right_line": 1383 + }, + { + "left_line": 1361, + "right_line": 1384 + }, + { + "left_line": 1362, + "right_line": 1385 + }, + { + "left_line": 1363, + "right_line": 1386 + }, + { + "left_line": 1364, + "right_line": 1387 + }, + { + "left_line": 1365, + "right_line": 1388 + }, + { + "left_line": 1366, + "right_line": 1389 + }, + { + "left_line": 1367, + "right_line": 1390 + }, + { + "left_line": 1368, + "right_line": 1391 + }, + { + "left_line": 1369, + "right_line": 1392 + }, + { + "left_line": 1370, + "right_line": 1393 + }, + { + "left_line": 1371, + "right_line": 1394 + }, + { + "left_line": 1372, + "right_line": 1395 + }, + { + "left_line": 1373, + "right_line": 1396 + }, + { + "left_line": 1374, + "right_line": 1397 + }, + { + "left_line": 1375, + "right_line": 1398 + }, + { + "left_line": 1376, + "right_line": 1399 + }, + { + "left_line": 1377, + "right_line": 1400 + }, + { + "left_line": 1378, + "right_line": 1401 + }, + { + "left_line": 1379, + "right_line": 1402 + }, + { + "left_line": 1380, + "right_line": 1403 + }, + { + "left_line": 1381, + "right_line": 1404 + }, + { + "left_line": 1382, + "right_line": 1405 + }, + { + "left_line": 1383, + "right_line": 1406 + }, + { + "left_line": 1384, + "right_line": 1407 + }, + { + "left_line": 1385, + "right_line": 1408 + }, + { + "left_line": 1386, + "right_line": 1409 + }, + { + "left_line": 1387, + "right_line": 1410 + }, + { + "left_line": 1388, + "right_line": 1411 + }, + { + "left_line": 1389, + "right_line": 1412 + }, + { + "left_line": 1390, + "right_line": 1413 + }, + { + "left_line": 1391, + "right_line": 1414 + }, + { + "left_line": 1392, + "right_line": 1415 + }, + { + "left_line": 1393, + "right_line": 1416 + }, + { + "left_line": 1394, + "right_line": 1417 + }, + { + "left_line": 1395, + "right_line": 1418 + }, + { + "left_line": 1396, + "right_line": 1419 + }, + { + "left_line": 1397, + "right_line": 1420 + }, + { + "left_line": 1398, + "right_line": 1421 + }, + { + "left_line": 1399, + "right_line": 1422 + }, + { + "left_line": 1400, + "right_line": 1423 + }, + { + "left_line": 1401, + "right_line": 1424 + }, + { + "left_line": 1402, + "right_line": 1425 + }, + { + "left_line": 1403, + "right_line": 1426 + }, + { + "left_line": 1404, + "right_line": 1427 + }, + { + "left_line": 1405, + "right_line": 1428 + }, + { + "left_line": 1406, + "right_line": 1429 + }, + { + "left_line": 1407, + "right_line": 1430 + }, + { + "left_line": 1408, + "right_line": 1431 + }, + { + "left_line": 1409, + "right_line": 1432 + }, + { + "left_line": 1410, + "right_line": 1433 + }, + { + "left_line": 1411, + "right_line": 1434 + }, + { + "left_line": 1412, + "right_line": 1435 + }, + { + "left_line": 1413, + "right_line": 1436 + }, + { + "left_line": 1414, + "right_line": 1437 + }, + { + "left_line": 1415, + "right_line": 1438 + }, + { + "left_line": 1416, + "right_line": 1439 + }, + { + "left_line": 1417, + "right_line": 1440 + }, + { + "left_line": 1418, + "right_line": 1441 + }, + { + "left_line": 1419, + "right_line": 1442 + }, + { + "left_line": 1420, + "right_line": 1443 + }, + { + "left_line": 1421, + "right_line": 1444 + }, + { + "left_line": 1422, + "right_line": 1445 + }, + { + "left_line": 1423, + "right_line": 1446 + }, + { + "left_line": 1424, + "right_line": 1447 + }, + { + "left_line": 1425, + "right_line": 1448 + }, + { + "left_line": 1426, + "right_line": 1449 + }, + { + "left_line": 1427, + "right_line": 1450 + }, + { + "left_line": 1428, + "right_line": 1451 + }, + { + "left_line": 1429, + "right_line": 1452 + }, + { + "left_line": 1430, + "right_line": 1453 + }, + { + "left_line": 1431, + "right_line": 1454 + }, + { + "left_line": 1432, + "right_line": 1455 + }, + { + "left_line": 1433, + "right_line": 1456 + }, + { + "left_line": 1434, + "right_line": 1457 + }, + { + "left_line": 1435, + "right_line": 1458 + }, + { + "left_line": 1436, + "right_line": 1459 + }, + { + "left_line": 1437, + "right_line": 1460 + }, + { + "left_line": 1438, + "right_line": 1461 + }, + { + "left_line": 1439, + "right_line": 1462 + }, + { + "left_line": 1440, + "right_line": 1463 + }, + { + "left_line": 1441, + "right_line": 1464 + }, + { + "left_line": 1442, + "right_line": 1465 + }, + { + "left_line": 1443, + "right_line": 1466 + }, + { + "left_line": 1444, + "right_line": 1467 + }, + { + "left_line": 1445, + "right_line": 1468 + }, + { + "left_line": 1446, + "right_line": 1469 + }, + { + "left_line": 1447, + "right_line": 1470 + }, + { + "left_line": 1448, + "right_line": 1471 + }, + { + "left_line": 1449, + "right_line": 1472 + }, + { + "left_line": 1450, + "right_line": 1473 + }, + { + "left_line": 1451, + "right_line": 1474 + }, + { + "left_line": 1452, + "right_line": 1475 + }, + { + "left_line": 1453, + "right_line": 1476 + }, + { + "left_line": 1454, + "right_line": 1477 + }, + { + "left_line": 1455, + "right_line": 1478 + }, + { + "left_line": 1456, + "right_line": 1479 + }, + { + "left_line": 1457, + "right_line": 1480 + }, + { + "left_line": 1458, + "right_line": 1481 + }, + { + "left_line": 1459, + "right_line": 1482 + }, + { + "left_line": 1460, + "right_line": 1483 + }, + { + "left_line": 1461, + "right_line": 1484 + }, + { + "left_line": 1462, + "right_line": 1485 + }, + { + "left_line": 1463, + "right_line": 1486 + }, + { + "left_line": 1464, + "right_line": 1487 + }, + { + "left_line": 1465, + "right_line": 1488 + }, + { + "left_line": 1466, + "right_line": 1489 + }, + { + "left_line": 1467, + "right_line": 1490 + }, + { + "left_line": 1468, + "right_line": 1491 + }, + { + "left_line": 1469, + "right_line": 1492 + }, + { + "left_line": 1470, + "right_line": 1493 + }, + { + "left_line": 1471, + "right_line": 1494 + }, + { + "left_line": 1472, + "right_line": 1495 + }, + { + "left_line": 1473, + "right_line": 1496 + }, + { + "left_line": 1474, + "right_line": 1497 + }, + { + "left_line": 1475, + "right_line": 1498 + }, + { + "left_line": 1476, + "right_line": 1499 + }, + { + "left_line": 1477, + "right_line": 1500 + }, + { + "left_line": 1478, + "right_line": 1501 + }, + { + "left_line": 1479, + "right_line": 1502 + }, + { + "left_line": 1480, + "right_line": 1503 + }, + { + "left_line": 1481, + "right_line": 1504 + }, + { + "left_line": 1482, + "right_line": 1505 + }, + { + "left_line": 1483, + "right_line": 1506 + }, + { + "left_line": 1484, + "right_line": 1507 + }, + { + "left_line": 1485, + "right_line": 1508 + }, + { + "left_line": 1486, + "right_line": 1509 + }, + { + "left_line": 1487, + "right_line": 1510 + }, + { + "left_line": 1488, + "right_line": 1511 + }, + { + "left_line": 1489, + "right_line": 1512 + }, + { + "left_line": 1490, + "right_line": 1513 + }, + { + "left_line": 1491, + "right_line": 1514 + }, + { + "left_line": 1492, + "right_line": 1515 + }, + { + "left_line": 1493, + "right_line": 1516 + }, + { + "left_line": 1494, + "right_line": 1517 + }, + { + "left_line": 1495, + "right_line": 1518 + }, + { + "left_line": 1496, + "right_line": 1519 + }, + { + "left_line": 1497, + "right_line": 1520 + }, + { + "left_line": 1498, + "right_line": 1521 + }, + { + "left_line": 1499, + "right_line": 1522 + }, + { + "left_line": 1500, + "right_line": 1523 + }, + { + "left_line": 1501, + "right_line": 1524 + }, + { + "left_line": 1502, + "right_line": 1525 + }, + { + "left_line": 1503, + "right_line": 1526 + }, + { + "left_line": 1504, + "right_line": 1527 + }, + { + "left_line": 1505, + "right_line": 1528 + }, + { + "left_line": 1506, + "right_line": 1529 + }, + { + "left_line": 1507, + "right_line": 1530 + }, + { + "left_line": 1508, + "right_line": 1531 + }, + { + "left_line": 1509, + "right_line": 1532 + }, + { + "left_line": 1510, + "right_line": 1533 + }, + { + "left_line": 1511, + "right_line": 1534 + }, + { + "left_line": 1512, + "right_line": 1535 + }, + { + "left_line": 1513, + "right_line": 1536 + }, + { + "left_line": 1514, + "right_line": 1537 + }, + { + "left_line": 1515, + "right_line": 1538 + }, + { + "left_line": 1516, + "right_line": 1539 + }, + { + "left_line": 1517, + "right_line": 1540 + }, + { + "left_line": 1518, + "right_line": 1541 + }, + { + "left_line": 1519, + "right_line": 1542 + }, + { + "left_line": 1520, + "right_line": 1543 + }, + { + "left_line": 1521, + "right_line": 1544 + }, + { + "left_line": 1522, + "right_line": 1545 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_add_missing_select/metadata.json b/tests/testdata/lua_kong_add_missing_select/metadata.json new file mode 100644 index 0000000..f301479 --- /dev/null +++ b/tests/testdata/lua_kong_add_missing_select/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "dfa83fb2b1dc2b1b0b6f49baa284829e7f120c5d", + "commit": "f4835f2", + "message": "fix(db): add missing select statement for export (#14290)", + "author": "Haoxuan", + "date": "2025-02-19T09:45:33+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 25, + "deletions": 2 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_add_missing_select/new.lua b/tests/testdata/lua_kong_add_missing_select/new.lua new file mode 100644 index 0000000..dad681a --- /dev/null +++ b/tests/testdata/lua_kong_add_missing_select/new.lua @@ -0,0 +1,1545 @@ +local arrays = require "pgmoon.arrays" +local json = require "pgmoon.json" +local cjson_safe = require "cjson.safe" +local new_tab = require "table.new" +local clear_tab = require "table.clear" + + +local kong = kong +local ngx = ngx +local encode_base64 = ngx.encode_base64 +local decode_base64 = ngx.decode_base64 +local encode_array = arrays.encode_array +local encode_json = json.encode_json +local setmetatable = setmetatable +local get_phase = ngx.get_phase +local tonumber = tonumber +local concat = table.concat +local insert = table.insert +local assert = assert +local ipairs = ipairs +local pairs = pairs +local error = error +local upper = string.upper +local pack = table.pack -- luacheck: ignore +local null = ngx.null +local type = type +local load = load +local find = string.find +local fmt = string.format +local rep = string.rep +local sub = string.sub +local log = ngx.log +local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy +local now_updated = require("kong.tools.time").get_updated_now + + +local NOTICE = ngx.NOTICE +local LIMIT = {} +local UNIQUE = {} + + +local function noop(...) + return ... +end + + +-- @param name Query name, for debugging purposes +-- @param query A string describing an array of single-quoted strings which +-- contain parts of an SQL query including numeric placeholders like $0, $1, etc. +-- All parts of that array were processed via using string.format("%q"), +-- so that all newlines and quotes are preserved. +-- @return Produces a function which, given an array of arguments, +-- interpolates them in the right places and outputs a ready-to-use SQL query. +local function compile(name, query) + local c = [=====[ + local v = ... or {} + return concat { ]=====] + .. query:gsub("$(%d+)", [[", v[%1], "]]) + .. [=====[ + } + ]=====] + return load(c, "=" .. name, "t", { concat = concat }) +end + + +local function expand(name, map) + local skip = {} + local c = { "local row = ... or {}\n" } + for _, field in ipairs(map) do + local entity = field.entity + if not skip[entity] then + skip[entity] = true + + local exps = {} + local keys = {} + local nils = {} + for _, key in ipairs(map) do + if key.entity == entity then + insert(exps, 'row["' .. key.from .. '"] ~= null') + if key.to ~= "ws_id" then + insert(keys, fmt('["%s"] = row["%s"]', key.to, key.from)) + end + insert(nils, fmt('row["%s"] = nil', key.from)) + end + end + + insert(c, (([[ + if $EXPS then + row["$ENTITY"] = { $KEYS } + else + row["$ENTITY"] = null + end + $NILS + ]]):gsub("$(%a+)", { + ENTITY = entity, + EXPS = concat(exps, " and "), + KEYS = concat(keys, ", "), + NILS = concat(nils, "; "), + }))) + end + end + insert(c, "return row") + + return load(concat(c), "=" .. name, "t", { null = null }) +end + + +local function collapse(name, map) + local skip = {} + local c = { [[ + local t = { ... } + local row = {} + for _, a in ipairs(t) do + for k, v in pairs(a) do + row[k] = v + end + end + ]] } + for _, field in ipairs(map) do + local entity = field.entity + if not skip[entity] then + skip[entity] = true + + local keys = {} + local nulls = {} + for _, key in ipairs(map) do + if key.entity == entity then + insert(keys, fmt('row["%s"] = row["%s"]["%s"]', key.from, entity, key.to)) + insert(nulls, fmt('row["%s"] = null', key.from)) + end + end + + insert(c, (([[ + if row["$ENTITY"] ~= nil and row["$ENTITY"] ~= null then + $KEYS + row["$ENTITY"] = nil + elseif row["$ENTITY"] == null then + $NULLS + row["$ENTITY"] = nil + end + ]]):gsub("$(%a+)", { + ENTITY = entity, + KEYS = concat(keys, "; "), + NULLS = concat(nulls, "; "), + }))) + end + end + insert(c, "return row") + local env = { ipairs = ipairs, pairs = pairs, null = null } + return load(concat(c), "=" .. name, "t", env) +end + + +local function escape_identifier(connector, identifier, field) + identifier = connector:escape_identifier(identifier) + + if field and field.timestamp then + return concat { "EXTRACT(EPOCH FROM ", identifier, " AT TIME ZONE 'UTC') AS ", identifier } + end + + return identifier +end + + +local function escape_literal(connector, literal, field) + if literal == nil or literal == null then + return "NULL" + end + + if field then + if field.timestamp then + return concat { "TO_TIMESTAMP(", connector:escape_literal(tonumber(fmt("%.3f", literal))), ") AT TIME ZONE 'UTC'" } + end + + if field.type == "integer" then + return fmt("%16.f", literal) + end + + if field.type == "array" or field.type == "set" then + if not literal[1] then + return connector:escape_literal("{}") + end + + local elements = field.elements + + if elements.timestamp then + local timestamps = {} + for i, v in ipairs(literal) do + timestamps[i] = concat { "TO_TIMESTAMP(", connector:escape_literal(tonumber(fmt("%.3f", v))), ") AT TIME ZONE 'UTC'" } + end + return encode_array(timestamps) + end + + local et = elements.type + + if et == "array" or et == "set" then + local el = elements + repeat + el = el.elements + et = el.type + until et ~= "array" and et ~= "set" + + if et == "map" or et == "record" then + return error("postgres strategy to escape multidimensional arrays of maps or records is not implemented") + end + + elseif et == "map" or et == "record" or et == "json" then + local jsons = {} + for i, v in ipairs(literal) do + jsons[i] = cjson_safe.encode(v) + end + return encode_array(jsons) .. '::JSONB[]' + + elseif et == "string" and elements.uuid then + return encode_array(literal) .. '::UUID[]' + end + + return encode_array(literal) + + elseif field.type == "map" or field.type == "record" or field.type == "json" then + return encode_json(literal) + end + end + + return connector:escape_literal(literal) +end + + +local function toerror(strategy, err, primary_key, entity) + local schema = strategy.schema + local errors = strategy.errors + + if find(err, "violates unique constraint", 1, true) then + log(NOTICE, err) + + if find(err, "cache_key", 1, true) then + local keys = {} + for _, k in ipairs(schema.cache_key) do + local field = schema.fields[k] + if field.type == "foreign" and entity[k] ~= null then + keys[k] = field.schema:extract_pk_values(entity[k]) + else + keys[k] = entity[k] + end + end + return nil, errors:unique_violation(keys) + end + + for field_name, field in schema:each_field() do + if field.unique then + if find(err, field_name, 1, true) then + return nil, errors:unique_violation({ + [field_name] = entity[field_name] + }) + end + end + end + + if not primary_key then + primary_key = {} + if entity then + for _, key in ipairs(schema.primary_key) do + primary_key[key] = entity[key] + end + end + end + return nil, errors:primary_key_violation(primary_key) + + elseif find(err, "violates not-null constraint", 1, true) then + -- not-null constraint is currently only enforced on primary key + log(NOTICE, err) + if not primary_key then + primary_key = {} + if entity then + for _, key in ipairs(schema.primary_key) do + primary_key[key] = entity[key] + end + end + end + return nil, errors:primary_key_violation(primary_key) + + elseif find(err, "violates foreign key constraint .*_ws_id_fkey") then + if schema.name == "workspaces" then + local found, e = find(err, "is still referenced from table", 1, true) + if not found then + return error("could not parse foreign key violation error message: " .. err) + end + + return nil, errors:foreign_key_violation_restricted(schema.name, sub(err, e + 3, -3)) + + else + local ws_id = err:match("ws_id%)=%(([^)]*)%)") or "null" + return nil, errors:invalid_workspace(ws_id) + end + + elseif find(err, "violates foreign key constraint", 1, true) then + log(NOTICE, err) + if find(err, "is not present in table", 1, true) then + local foreign_field_name + local foreign_schema + for field_name, field in schema:each_field() do + if field.type == "foreign" then + local escaped_identifier = escape_identifier(strategy.connector, + field.schema.name) + + if find(err, escaped_identifier, 1, true) then + foreign_field_name = field_name + foreign_schema = field.schema + break + end + end + end + + if not foreign_schema then + return error("could not determine foreign schema for violated foreign key error") + end + + local foreign_key = {} + for _, key in ipairs(foreign_schema.primary_key) do + if entity[foreign_field_name] then + foreign_key[key] = entity[foreign_field_name][key] + + else + if primary_key[key] then + foreign_key[key] = primary_key[key] + + elseif primary_key[foreign_field_name] then + foreign_key[key] = primary_key[foreign_field_name][key] + end + end + end + + return nil, errors:foreign_key_violation_invalid_reference(foreign_key, + foreign_field_name, + foreign_schema.name) + + else + local found, e = find(err, "is still referenced from table", 1, true) + if not found then + return error("could not parse foreign key violation error message: " .. err) + end + + return nil, errors:foreign_key_violation_restricted(schema.name, sub(err, e + 3, -3)) + end + end + + return nil, errors:database_error(err) +end + + +local function get_ttl_value(strategy, attributes, options) + local ttl_value = options and options.ttl + local is_update = options and options.update + local fields = strategy.fields + + if ttl_value == 0 or not ttl_value then + return null + end + + if not is_update and + attributes.created_at and + fields.created_at and + fields.created_at.timestamp and + fields.created_at.auto then + return ttl_value + attributes.created_at + end + + if is_update and + attributes.updated_at and + fields.updated_at and + fields.updated_at.timestamp and + fields.updated_at.auto then + return ttl_value + attributes.updated_at + end + + return ttl_value + now_updated() +end + + +local function get_ws_id() + local phase = get_phase() + if phase ~= "init" and phase ~= "init_worker" then + return ngx.ctx.workspace or kong.default_workspace + end +end + + +local function execute(strategy, statement_name, attributes, options) + local ws_id + local has_ws_id = strategy.schema.workspaceable + if has_ws_id then + if options and options.workspace then + if options.workspace ~= null then + ws_id = options.workspace + end + else + ws_id = get_ws_id() + end + + if not ws_id then + statement_name = statement_name .. "_global" + end + end + + local connector = strategy.connector + local statement = strategy.statements[statement_name] + if not attributes then + return connector:query(statement[1], statement[2]) + end + + local fields = strategy.fields + local argn = statement.argn + local argv = statement.argv + local argc = statement.argc + + clear_tab(argv) + + local is_update = options and options.update + local has_ttl = strategy.schema.ttl + local skip_ttl = options and options.skip_ttl + if has_ws_id then + assert(ws_id == nil or type(ws_id) == "string") + argv[0] = escape_literal(connector, ws_id, "ws_id") + end + + for i = 1, argc do + local name = argn[i] + local value + if has_ttl and name == "ttl" and not skip_ttl then + value = (options and options.ttl) + and get_ttl_value(strategy, attributes, options) + + elseif i == argc and is_update and attributes[UNIQUE] then + value = attributes[UNIQUE] + if type(value) == "table" then + value = value[name] + end + + else + value = attributes[name] + end + + argv[i] = (value == nil and is_update) + and escape_identifier(connector, name) + or escape_literal(connector, value, fields[name]) + end + + local sql = statement.make(argv) + return connector:query(sql, statement.operation) +end + + +local function page(self, size, token, foreign_key, foreign_entity_name, options) + if not size then + size = self.connector:get_page_size(options) + end + + local limit = size + 1 + + local statement_name + local attributes = { + [LIMIT] = limit, + } + + local suffix = token and "_next" or "_first" + if foreign_entity_name then + statement_name = "page_for_" .. foreign_entity_name .. suffix + attributes[foreign_entity_name] = foreign_key + + elseif options and options.tags then + statement_name = options.tags_cond == "or" and + "page_by_tags_or" .. suffix or + "page_by_tags_and" .. suffix + attributes.tags = options.tags + + else + statement_name = "page" .. suffix + end + + if options and options.export then + statement_name = statement_name .. "_for_export" + end + + if token then + local token_decoded = decode_base64(token) + if not token_decoded then + return nil, self.errors:invalid_offset(token, "bad base64 encoding") + end + + token_decoded = cjson_safe.decode(token_decoded) + if not token_decoded then + return nil, self.errors:invalid_offset(token, "bad json encoding") + end + + for i, field_name in ipairs(self.schema.primary_key) do + attributes[field_name] = token_decoded[i] + end + end + + local res, err = execute(self, statement_name, self.collapse(attributes), options) + + if not res then + return toerror(self, err) + end + + local rows = new_tab(size, 0) + + for i = 1, limit do + local row = res[i] + if not row then + break + end + + if i == limit then + row = res[size] + local offset = {} + for _, field_name in ipairs(self.schema.primary_key) do + insert(offset, row[field_name]) + end + + offset = cjson_safe.encode(offset) + offset = encode_base64(offset, true) + + return rows, nil, offset + end + + rows[i] = self.expand(row) + end + + return rows +end + + +local function make_select_for(foreign_entity_name) + return function(self, foreign_key, size, token, options) + return page(self, size, token, foreign_key, foreign_entity_name, options) + end +end + + +local _mt = {} + + +_mt.__index = _mt + + +function _mt:truncate(options) + local res, err = execute(self, "truncate", nil, options) + if not res then + return toerror(self, err) + end + return true, nil +end + + +function _mt:insert(entity, options) + local res, err = execute(self, "insert", self.collapse(entity), options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + + return nil, nil + end + + return toerror(self, err, nil, entity) +end + + +function _mt:select(primary_key, options) + local statement_name = "select" + if options then + -- If we do select for export, there is no need to skip ttl. + if options.export then + statement_name = "select_for_export" + + elseif self.schema.ttl and options.skip_ttl then + statement_name = "select_skip_ttl" + end + end + + local res, err = execute(self, statement_name, self.collapse(primary_key), options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + + return nil, nil + end + + return toerror(self, err, primary_key) +end + + +function _mt:select_by_field(field_name, unique_value, options) + local statement_name = "select_by_" .. field_name + + if self.schema.ttl and options and options.skip_ttl then + statement_name = statement_name .. "_skip_ttl" + end + + local filter = { + [field_name] = unique_value, + } + + local res, err = execute(self, statement_name, self.collapse(filter), options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + + return nil, nil + end + + return toerror(self, err, filter) +end + + +local function update_options(options) + return { + update = true, + ttl = options and options.ttl, + workspace = options and options.workspace ~= null and options.workspace, + } +end + + +function _mt:update(primary_key, entity, options) + local res, err = execute(self, "update", + self.collapse(primary_key, entity), + update_options(options)) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found(primary_key) + end + + return toerror(self, err, primary_key, entity) +end + + +function _mt:update_by_field(field_name, unique_value, entity, options) + local filter + if type(unique_value) == "table" then + filter = self.collapse({ [field_name] = unique_value }) + else + filter = unique_value + end + + local res, err = execute(self, "update_by_" .. field_name, + self.collapse({ [UNIQUE] = filter }, entity), + update_options(options)) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found_by_field { + [field_name] = unique_value, + } + end + + return toerror(self, err, { [field_name] = unique_value }, entity) +end + + +function _mt:upsert(primary_key, entity, options) + local collapsed_entity = self.collapse(entity, primary_key) + local res, err = execute(self, "upsert", collapsed_entity, options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found(primary_key) + end + + return toerror(self, err, primary_key, entity) +end + + +function _mt:upsert_by_field(field_name, unique_value, entity, options) + local collapsed_entity = self.collapse(entity, { + [field_name] = unique_value + }) + local res, err = execute(self, "upsert_by_" .. field_name, collapsed_entity, options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found_by_field { + [field_name] = unique_value, + } + end + + return toerror(self, err, { [field_name] = unique_value }, entity) +end + + +function _mt:delete(primary_key, options) + local statement_name = "delete" + if self.schema.ttl and options and options.skip_ttl then + statement_name = "delete_skip_ttl" + end + local res, err = execute(self, statement_name, self.collapse(primary_key), options) + if res then + if res.affected_rows == 0 then + return nil, nil + end + + return true, nil + end + + return toerror(self, err, primary_key) +end + + +function _mt:delete_by_field(field_name, unique_value, options) + local statement_name = "delete_by_" .. field_name + if self.schema.ttl and options and options.skip_ttl then + statement_name = statement_name .. "_skip_ttl" + end + local filter = { + [field_name] = unique_value, + } + + local res, err = execute(self, statement_name, self.collapse(filter), options) + + if res then + if res.affected_rows == 0 then + return nil, nil + end + + return true, nil + end + + return toerror(self, err, filter) +end + + +function _mt:page(size, token, options) + return page(self, size, token, nil, nil, options) +end + + +function _mt:escape_literal(literal, field_name) + return escape_literal(self.connector, literal, self.fields[field_name]) +end + + +local function format_on_condition(on_condition) + if not on_condition then + return nil + end + on_condition = upper(on_condition) + if on_condition ~= "RESTRICT" and + on_condition ~= "CASCADE" and + on_condition ~= "NULL" and + on_condition ~= "DEFAULT" then + on_condition = nil + end + return on_condition +end + + +local function where_clause(where, ...) + local inputs = pack(...) + return function(add_ws) + local exps = {} + for i = 1, inputs.n do + local exp = inputs[i] + if exp then + if add_ws then + insert(exps, exp) + else + if not exp:match("%(\"ws_id\" = ") then + insert(exps, exp) + end + end + end + end + + if #exps == 0 then + return "" + end + + return where .. concat(exps, "\n" .. rep(" ", #where - 4) .. "AND ") .. "\n" + end +end + + +local function conflict_list(has_ws_id, ...) + local inputs = pack(...) + return function(add_ws) + local exps = inputs + if add_ws and has_ws_id then + insert(exps, 1, '"ws_id"') + end + + return concat(exps, ", ") + end +end + + +-- placeholders in queries must always be constructed as a single string, +-- to avoid ending up genering code like this: `concat { "... $", "2 AND ..." }` +local function placeholder(n) + return "$" .. n +end + + +local _M = {} + + +function _M.new(connector, schema, errors) + local primary_key = schema.primary_key + local primary_key_fields = {} + + for _, field_name in ipairs(primary_key) do + primary_key_fields[field_name] = true + end + + local has_ttl = schema.ttl == true + local has_tags = schema.fields.tags ~= nil + local has_composite_cache_key = schema.cache_key and #schema.cache_key > 1 + local has_ws_id = schema.workspaceable == true + local fields = {} + local fields_hash = {} + + local table_name = schema.table_name + local table_name_escaped = escape_identifier(connector, table_name) + + local foreign_key_list = {} + local foreign_keys = {} + + local unique_fields = {} + + + for field_name, field in schema:each_field() do + if field.transient then + goto continue + end + + if field.type == "foreign" then + local foreign_schema = field.schema + local foreign_key_names = {} + local foreign_key_escaped = {} + local foreign_col_names = {} + local is_unique_foreign = field.unique == true + + for _, foreign_field_name in ipairs(foreign_schema.primary_key) do + local foreign_field + for foreign_schema_field_name, foreign_schema_field in foreign_schema:each_field() do + if foreign_schema_field_name == foreign_field_name then + foreign_field = foreign_schema_field + break + end + end + + local name = field_name .. "_" .. foreign_field_name + + fields_hash[name] = foreign_field + + local prepared_field = { + referenced_table = foreign_schema.name, + referenced_column = foreign_field_name, + on_update = format_on_condition(field.on_update), + on_delete = format_on_condition(field.on_delete), + name = name, + name_escaped = escape_identifier(connector, name), + name_expression = escape_identifier(connector, name, foreign_field), + field_name = field_name, + is_used_in_primary_key = primary_key_fields[field_name] ~= nil, + is_part_of_composite_key = #foreign_schema.primary_key > 1, + is_unique = foreign_field.unique == true, + is_unique_across_ws = foreign_field.unique_across_ws == true, + is_endpoint_key = schema.endpoint_key == field_name, + is_unique_foreign = is_unique_foreign, + } + + if prepared_field.is_used_in_primary_key then + primary_key_fields[field_name] = prepared_field + end + + insert(fields, prepared_field) + + insert(foreign_key_names, name) + insert(foreign_key_escaped, prepared_field.name_escaped) + insert(foreign_col_names, escape_identifier(connector, foreign_field_name)) + insert(foreign_key_list, { + from = name, + entity = field_name, + to = foreign_field_name + }) + end + + foreign_keys[field_name] = { + names = foreign_key_names, + escaped = foreign_key_escaped, + } + + else + fields_hash[field_name] = field + + local is_used_in_primary_key = primary_key_fields[field_name] ~= nil + local is_part_of_composite_key = is_used_in_primary_key and #primary_key > 1 or false + + local prepared_field = { + name = field_name, + name_escaped = escape_identifier(connector, field_name), + name_expression = escape_identifier(connector, field_name, field), + is_used_in_primary_key = is_used_in_primary_key, + is_part_of_composite_key = is_part_of_composite_key, + is_unique = field.unique == true, + is_unique_across_ws = field.unique_across_ws == true, + is_endpoint_key = schema.endpoint_key == field_name, + } + + if prepared_field.is_used_in_primary_key then + primary_key_fields[field_name] = prepared_field + end + + insert(fields, prepared_field) + end + ::continue:: + end + + local primary_key_names = {} + local primary_key_placeholders = {} + local insert_names = {} + local insert_columns = {} + local insert_expressions = {} + local select_expressions = {} + local update_expressions = {} + local update_names = {} + local update_placeholders = {} + local upsert_expressions = {} + local page_next_names = {} + + for i, field in ipairs(fields) do + local name = field.name + local name_escaped = field.name_escaped + local name_expression = field.name_expression + local is_used_in_primary_key = field.is_used_in_primary_key + local is_part_of_composite_key = field.is_part_of_composite_key + local is_unique = field.is_unique + local is_unique_foreign = field.is_unique_foreign + local is_endpoint_key = field.is_endpoint_key + local referenced_table = field.referenced_table + + insert(insert_names, name) + insert(insert_columns, name_escaped) + insert(insert_expressions, "$" .. i) + insert(select_expressions, name_expression) + + if not is_used_in_primary_key then + insert(update_names, name) + insert(update_expressions, name_escaped .. " = $" .. #update_names) + insert(upsert_expressions, name_escaped .. " = " .. "EXCLUDED." .. name_escaped) + end + + if ((not is_used_in_primary_key) or is_part_of_composite_key) + and ((referenced_table and not is_part_of_composite_key) + or is_unique_foreign + or is_unique + or (is_endpoint_key and not is_unique)) + then + -- treat endpoint_key like a unique key anyway, + -- they are indexed (example: target.target) + insert(unique_fields, field) + end + end + + local update_args_names = {} + + for _, update_name in ipairs(update_names) do + insert(update_args_names, update_name) + end + + local cache_key_escaped + if has_composite_cache_key then + cache_key_escaped = escape_identifier(connector, "cache_key") + insert(update_names, "cache_key") + insert(update_args_names, "cache_key") + insert(update_expressions, cache_key_escaped .. " = $" .. #update_names) + insert(upsert_expressions, cache_key_escaped .. " = " .. "EXCLUDED." .. cache_key_escaped) + end + + local ws_id_escaped + if has_ws_id then + ws_id_escaped = escape_identifier(connector, "ws_id") + insert(select_expressions, ws_id_escaped) + insert(update_names, "ws_id") + insert(update_args_names, "ws_id") + insert(update_expressions, ws_id_escaped .. " = $0") + insert(upsert_expressions, ws_id_escaped .. " = " .. "EXCLUDED." .. ws_id_escaped) + end + + local ttl_escaped + if has_ttl then + ttl_escaped = escape_identifier(connector, "ttl") + insert(update_names, "ttl") + insert(update_args_names, "ttl") + insert(update_expressions, ttl_escaped .. " = $" .. #update_names) + insert(upsert_expressions, ttl_escaped .. " = " .. "EXCLUDED." .. ttl_escaped) + end + + local primary_key_escaped = {} + for i, key in ipairs(primary_key) do + local primary_key_field = primary_key_fields[key] + + insert(page_next_names, primary_key_field.name) + insert(primary_key_names, primary_key_field.name) + insert(primary_key_escaped, primary_key_field.name_escaped) + insert(update_args_names, primary_key_field.name) + insert(update_placeholders, "$" .. #update_args_names) + insert(primary_key_placeholders, "$" .. i) + end + + insert(page_next_names, LIMIT) + + local pk_escaped = concat(primary_key_escaped, ", ") + + select_expressions = concat(select_expressions, ", ") + primary_key_placeholders = concat(primary_key_placeholders, ", ") + update_placeholders = concat(update_placeholders, ", ") + + if has_composite_cache_key then + fields_hash.cache_key = { type = "string" } + + insert(insert_names, "cache_key") + insert(insert_expressions, "$" .. #insert_names) + insert(insert_columns, cache_key_escaped) + end + + local ws_id_select_where + if has_ws_id then + fields_hash.ws_id = { type = "string", uuid = true } + + insert(insert_names, "ws_id") + insert(insert_expressions, "$0") + insert(insert_columns, ws_id_escaped) + + ws_id_select_where = "(" .. ws_id_escaped .. " = $0)" + end + + local select_for_export_expressions + local ttl_select_where + if has_ttl then + fields_hash.ttl = { timestamp = true } + + insert(insert_names, "ttl") + insert(insert_expressions, "$" .. #insert_names) + insert(insert_columns, ttl_escaped) + + select_for_export_expressions = concat { + select_expressions, ",", + "FLOOR(EXTRACT(EPOCH FROM (", + ttl_escaped, " AT TIME ZONE 'UTC'", + "))) AS ", ttl_escaped + } + + select_expressions = concat { + select_expressions, ",", + "FLOOR(EXTRACT(EPOCH FROM (", + ttl_escaped, " AT TIME ZONE 'UTC' - CURRENT_TIMESTAMP AT TIME ZONE 'UTC'", + "))) AS ", ttl_escaped + } + + ttl_select_where = concat { + "(", ttl_escaped, " IS NULL OR ", ttl_escaped, " >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC')" + } + end + + insert_expressions = concat(insert_expressions, ", ") + insert_columns = concat(insert_columns, ", ") + + update_expressions = concat(update_expressions, ", ") + + upsert_expressions = concat(upsert_expressions, ", ") + + local primary_key_args = {} + local insert_args = {} + local update_args = {} + local single_args = {} + local page_next_args = {} + + local self = setmetatable({ + connector = connector, + schema = schema, + errors = errors, + expand = #foreign_key_list > 0 and + expand(table_name .. "_expand", foreign_key_list) or + noop, + collapse = collapse(table_name .. "_collapse", foreign_key_list), + fields = fields_hash, + + statements = { + truncate = { + concat { + "TRUNCATE ", table_name_escaped, " RESTART IDENTITY CASCADE;" + }, + "write", + }, + } + }, _mt) + + self.statements["truncate_global"] = self.statements["truncate"] + + local add_statement + local add_statement_for_export + do + local function add(name, opts, add_ws) + local orig_argn = opts.argn + opts = cycle_aware_deep_copy(opts) + + -- ensure LIMIT table is the same + for i, n in ipairs(orig_argn) do + if type(n) == "table" then + opts.argn[i] = n + end + end + + for i = 1, #opts.code do + if type(opts.code[i]) == "function" then + opts.code[i] = opts.code[i](add_ws) + end + opts.code[i] = fmt("%q", opts.code[i]) + end + opts.make = compile(table_name .. "_" .. name, concat(opts.code, ", ")) + opts.code = nil + opts.argc = #opts.argn + self.statements[name] = opts + end + + add_statement = function(name, opts) + add(name .. "_global", opts, false) + add(name, opts, true) + end + + add_statement_for_export = function(name, opts) + add_statement(name, opts) + if has_ttl then + opts.code[2] = select_for_export_expressions + add_statement(name .. "_for_export", opts) + end + end + end + + add_statement("insert", { + operation = "write", + expr = insert_expressions, + cols = insert_columns, + argn = insert_names, + argv = insert_args, + code = { + "INSERT INTO ", table_name_escaped, " (", insert_columns, ")\n", + " VALUES (", insert_expressions, ")\n", + " RETURNING ", select_expressions, ";", + } + }) + + add_statement("upsert", { + operation = "write", + expr = upsert_expressions, + argn = insert_names, + argv = insert_args, + code = { + "INSERT INTO ", table_name_escaped, " (", insert_columns, ")\n", + " VALUES (", insert_expressions, ")\n", + "ON CONFLICT (", pk_escaped, ") DO UPDATE\n", + " SET ", upsert_expressions, "\n", + " RETURNING ", select_expressions, ";", + } + }) + + add_statement("update", { + operation = "write", + expr = update_expressions, + argn = update_args_names, + argv = update_args, + code = { + " UPDATE ", table_name_escaped, "\n", + " SET ", update_expressions, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. update_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "RETURNING ", select_expressions, ";", + } + }) + + add_statement("delete", { + operation = "write", + argn = primary_key_names, + argv = primary_key_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), ";" + } + }) + + add_statement("delete_skip_ttl", { + operation = "write", + argn = primary_key_names, + argv = primary_key_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ws_id_select_where), ";" + } + }) + + add_statement("select", { + operation = "read", + expr = select_expressions, + argn = primary_key_names, + argv = primary_key_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + " LIMIT 1;" + } + }) + + add_statement("select_skip_ttl", { + operation = "read", + expr = select_expressions, + argn = primary_key_names, + argv = primary_key_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ws_id_select_where), + " LIMIT 1;" + } + }) + + -- Add statements for exporting database, avoiding exporting TTL in absolute value. + add_statement_for_export("select", { + operation = "read", + expr = select_expressions, + argn = primary_key_names, + argv = primary_key_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + " LIMIT 1;" + } + }) + + add_statement_for_export("page_first", { + operation = "read", + argn = { LIMIT }, + argv = single_args, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT $1;"; + } + }) + + add_statement_for_export("page_next", { + operation = "read", + argn = page_next_names, + argv = page_next_args, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") > (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT " .. placeholder(#page_next_names), ";" + } + }) + + if #foreign_key_list > 0 then + for foreign_entity_name, foreign_key in pairs(foreign_keys) do + local fk_names = foreign_key.names + local fk_escaped = foreign_key.escaped + + local fk_placeholders = {} + local pk_placeholders = {} + + local foreign_key_names = concat(fk_escaped, ", ") + + local argv_first = {} + local argn_first = {} + local argv_next = {} + local argn_next = {} + + for i, fk_name in ipairs(fk_names) do + insert(argn_first, fk_name) + insert(argn_next, fk_name) + insert(fk_placeholders, placeholder(i)) + end + + for i, primary_key_name in ipairs(primary_key_names) do + insert(argn_next, primary_key_name) + insert(pk_placeholders, placeholder(i + #fk_names)) + end + + insert(argn_first, LIMIT) + insert(argn_next, LIMIT) + + fk_placeholders = concat(fk_placeholders, ", ") + pk_placeholders = concat(pk_placeholders, ", ") + + local statement_name = "page_for_" .. foreign_entity_name + + add_statement_for_export(statement_name .. "_first", { + operation = "read", + argn = argn_first, + argv = argv_first, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. foreign_key_names .. ") = (" .. fk_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT ", placeholder(#argn_first), ";"; + } + }) + + add_statement_for_export(statement_name .. "_next", { + operation = "read", + argn = argn_next, + argv = argv_next, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. foreign_key_names .. ") = (" .. fk_placeholders .. ")", + "(" .. pk_escaped .. ") > (" .. pk_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT ", placeholder(#argn_next), ";" + } + }) + + self[statement_name] = make_select_for(foreign_entity_name) + end + end + + if has_tags then + local pk_placeholders = {} + + local argn_first = { "tags", LIMIT } + local argn_next = { "tags" } + + for i, primary_key_name in ipairs(primary_key_names) do + insert(argn_next, primary_key_name) + pk_placeholders[i] = placeholder(i + 1) + end + insert(argn_next, LIMIT) + + for cond, op in pairs({["_and"] = "@>", ["_or"] = "&&"}) do + + add_statement_for_export("page_by_tags" .. cond .. "_first", { + operation = "read", + argn = argn_first, + argv = {}, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "tags " .. op .. " $1", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT $2;"; + }, + }) + + add_statement_for_export("page_by_tags" .. cond .. "_next", { + operation = "read", + argn = argn_next, + argv = {}, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "tags " .. op .. " $1", + "(" .. pk_escaped .. ") > (" .. concat(pk_placeholders, ", ") .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT ", placeholder(#argn_next), ";" + } + }) + end + end + + if has_composite_cache_key then + insert(unique_fields, { + name = "cache_key", + name_escaped = escape_identifier(connector, "cache_key"), + }) + end + + if #unique_fields > 0 then + local update_by_args = {} + + for _, unique_field in ipairs(unique_fields) do + local field_name = unique_field.field_name or unique_field.name + local unique_name = unique_field.name + local unique_escaped = unique_field.name_escaped + local single_names = { unique_name } + + add_statement("select_by_" .. field_name, { + operation = "read", + argn = single_names, + argv = single_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ttl_select_where, + ws_id_select_where), + " LIMIT 1;" + }, + }) + + add_statement("select_by_" .. field_name .. "_skip_ttl", { + operation = "read", + argn = single_names, + argv = single_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ws_id_select_where), + " LIMIT 1;" + }, + }) + + local update_by_args_names = {} + for _, update_name in ipairs(update_names) do + insert(update_by_args_names, update_name) + end + + insert(update_by_args_names, unique_name) + + add_statement("update_by_" .. field_name, { + operation = "write", + argn = update_by_args_names, + argv = update_by_args, + code = { + " UPDATE ", table_name_escaped, "\n", + " SET ", update_expressions, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $" .. #update_names + 1, + ttl_select_where, + ws_id_select_where), + "RETURNING ", select_expressions, ";", + } + }) + + local conflict_key = unique_escaped + if has_composite_cache_key and not unique_field.is_endpoint_key then + conflict_key = escape_identifier(connector, "cache_key") + end + + local use_ws_id = has_ws_id and not unique_field.is_unique_across_ws + + add_statement("upsert_by_" .. field_name, { + operation = "write", + argn = insert_names, + argv = insert_args, + code = { + "INSERT INTO ", table_name_escaped, " (", insert_columns, ")\n", + " VALUES (", insert_expressions, ")\n", + "ON CONFLICT (", conflict_list(use_ws_id, conflict_key), ") DO UPDATE\n", + " SET ", upsert_expressions, "\n", + " RETURNING ", select_expressions, ";", + } + }) + + add_statement("delete_by_" .. field_name, { + operation = "write", + argn = single_names, + argv = single_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ttl_select_where, + ws_id_select_where), ";" + } + }) + + add_statement("delete_by_" .. field_name .. "_skip_ttl", { + operation = "write", + argn = single_names, + argv = single_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ws_id_select_where), ";" + } + }) + end + end + + return self +end + + +return _M diff --git a/tests/testdata/lua_kong_add_missing_select/old.lua b/tests/testdata/lua_kong_add_missing_select/old.lua new file mode 100644 index 0000000..c751b4b --- /dev/null +++ b/tests/testdata/lua_kong_add_missing_select/old.lua @@ -0,0 +1,1522 @@ +local arrays = require "pgmoon.arrays" +local json = require "pgmoon.json" +local cjson_safe = require "cjson.safe" +local new_tab = require "table.new" +local clear_tab = require "table.clear" + + +local kong = kong +local ngx = ngx +local encode_base64 = ngx.encode_base64 +local decode_base64 = ngx.decode_base64 +local encode_array = arrays.encode_array +local encode_json = json.encode_json +local setmetatable = setmetatable +local get_phase = ngx.get_phase +local tonumber = tonumber +local concat = table.concat +local insert = table.insert +local assert = assert +local ipairs = ipairs +local pairs = pairs +local error = error +local upper = string.upper +local pack = table.pack -- luacheck: ignore +local null = ngx.null +local type = type +local load = load +local find = string.find +local fmt = string.format +local rep = string.rep +local sub = string.sub +local log = ngx.log +local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy +local now_updated = require("kong.tools.time").get_updated_now + + +local NOTICE = ngx.NOTICE +local LIMIT = {} +local UNIQUE = {} + + +local function noop(...) + return ... +end + + +-- @param name Query name, for debugging purposes +-- @param query A string describing an array of single-quoted strings which +-- contain parts of an SQL query including numeric placeholders like $0, $1, etc. +-- All parts of that array were processed via using string.format("%q"), +-- so that all newlines and quotes are preserved. +-- @return Produces a function which, given an array of arguments, +-- interpolates them in the right places and outputs a ready-to-use SQL query. +local function compile(name, query) + local c = [=====[ + local v = ... or {} + return concat { ]=====] + .. query:gsub("$(%d+)", [[", v[%1], "]]) + .. [=====[ + } + ]=====] + return load(c, "=" .. name, "t", { concat = concat }) +end + + +local function expand(name, map) + local skip = {} + local c = { "local row = ... or {}\n" } + for _, field in ipairs(map) do + local entity = field.entity + if not skip[entity] then + skip[entity] = true + + local exps = {} + local keys = {} + local nils = {} + for _, key in ipairs(map) do + if key.entity == entity then + insert(exps, 'row["' .. key.from .. '"] ~= null') + if key.to ~= "ws_id" then + insert(keys, fmt('["%s"] = row["%s"]', key.to, key.from)) + end + insert(nils, fmt('row["%s"] = nil', key.from)) + end + end + + insert(c, (([[ + if $EXPS then + row["$ENTITY"] = { $KEYS } + else + row["$ENTITY"] = null + end + $NILS + ]]):gsub("$(%a+)", { + ENTITY = entity, + EXPS = concat(exps, " and "), + KEYS = concat(keys, ", "), + NILS = concat(nils, "; "), + }))) + end + end + insert(c, "return row") + + return load(concat(c), "=" .. name, "t", { null = null }) +end + + +local function collapse(name, map) + local skip = {} + local c = { [[ + local t = { ... } + local row = {} + for _, a in ipairs(t) do + for k, v in pairs(a) do + row[k] = v + end + end + ]] } + for _, field in ipairs(map) do + local entity = field.entity + if not skip[entity] then + skip[entity] = true + + local keys = {} + local nulls = {} + for _, key in ipairs(map) do + if key.entity == entity then + insert(keys, fmt('row["%s"] = row["%s"]["%s"]', key.from, entity, key.to)) + insert(nulls, fmt('row["%s"] = null', key.from)) + end + end + + insert(c, (([[ + if row["$ENTITY"] ~= nil and row["$ENTITY"] ~= null then + $KEYS + row["$ENTITY"] = nil + elseif row["$ENTITY"] == null then + $NULLS + row["$ENTITY"] = nil + end + ]]):gsub("$(%a+)", { + ENTITY = entity, + KEYS = concat(keys, "; "), + NULLS = concat(nulls, "; "), + }))) + end + end + insert(c, "return row") + local env = { ipairs = ipairs, pairs = pairs, null = null } + return load(concat(c), "=" .. name, "t", env) +end + + +local function escape_identifier(connector, identifier, field) + identifier = connector:escape_identifier(identifier) + + if field and field.timestamp then + return concat { "EXTRACT(EPOCH FROM ", identifier, " AT TIME ZONE 'UTC') AS ", identifier } + end + + return identifier +end + + +local function escape_literal(connector, literal, field) + if literal == nil or literal == null then + return "NULL" + end + + if field then + if field.timestamp then + return concat { "TO_TIMESTAMP(", connector:escape_literal(tonumber(fmt("%.3f", literal))), ") AT TIME ZONE 'UTC'" } + end + + if field.type == "integer" then + return fmt("%16.f", literal) + end + + if field.type == "array" or field.type == "set" then + if not literal[1] then + return connector:escape_literal("{}") + end + + local elements = field.elements + + if elements.timestamp then + local timestamps = {} + for i, v in ipairs(literal) do + timestamps[i] = concat { "TO_TIMESTAMP(", connector:escape_literal(tonumber(fmt("%.3f", v))), ") AT TIME ZONE 'UTC'" } + end + return encode_array(timestamps) + end + + local et = elements.type + + if et == "array" or et == "set" then + local el = elements + repeat + el = el.elements + et = el.type + until et ~= "array" and et ~= "set" + + if et == "map" or et == "record" then + return error("postgres strategy to escape multidimensional arrays of maps or records is not implemented") + end + + elseif et == "map" or et == "record" or et == "json" then + local jsons = {} + for i, v in ipairs(literal) do + jsons[i] = cjson_safe.encode(v) + end + return encode_array(jsons) .. '::JSONB[]' + + elseif et == "string" and elements.uuid then + return encode_array(literal) .. '::UUID[]' + end + + return encode_array(literal) + + elseif field.type == "map" or field.type == "record" or field.type == "json" then + return encode_json(literal) + end + end + + return connector:escape_literal(literal) +end + + +local function toerror(strategy, err, primary_key, entity) + local schema = strategy.schema + local errors = strategy.errors + + if find(err, "violates unique constraint", 1, true) then + log(NOTICE, err) + + if find(err, "cache_key", 1, true) then + local keys = {} + for _, k in ipairs(schema.cache_key) do + local field = schema.fields[k] + if field.type == "foreign" and entity[k] ~= null then + keys[k] = field.schema:extract_pk_values(entity[k]) + else + keys[k] = entity[k] + end + end + return nil, errors:unique_violation(keys) + end + + for field_name, field in schema:each_field() do + if field.unique then + if find(err, field_name, 1, true) then + return nil, errors:unique_violation({ + [field_name] = entity[field_name] + }) + end + end + end + + if not primary_key then + primary_key = {} + if entity then + for _, key in ipairs(schema.primary_key) do + primary_key[key] = entity[key] + end + end + end + return nil, errors:primary_key_violation(primary_key) + + elseif find(err, "violates not-null constraint", 1, true) then + -- not-null constraint is currently only enforced on primary key + log(NOTICE, err) + if not primary_key then + primary_key = {} + if entity then + for _, key in ipairs(schema.primary_key) do + primary_key[key] = entity[key] + end + end + end + return nil, errors:primary_key_violation(primary_key) + + elseif find(err, "violates foreign key constraint .*_ws_id_fkey") then + if schema.name == "workspaces" then + local found, e = find(err, "is still referenced from table", 1, true) + if not found then + return error("could not parse foreign key violation error message: " .. err) + end + + return nil, errors:foreign_key_violation_restricted(schema.name, sub(err, e + 3, -3)) + + else + local ws_id = err:match("ws_id%)=%(([^)]*)%)") or "null" + return nil, errors:invalid_workspace(ws_id) + end + + elseif find(err, "violates foreign key constraint", 1, true) then + log(NOTICE, err) + if find(err, "is not present in table", 1, true) then + local foreign_field_name + local foreign_schema + for field_name, field in schema:each_field() do + if field.type == "foreign" then + local escaped_identifier = escape_identifier(strategy.connector, + field.schema.name) + + if find(err, escaped_identifier, 1, true) then + foreign_field_name = field_name + foreign_schema = field.schema + break + end + end + end + + if not foreign_schema then + return error("could not determine foreign schema for violated foreign key error") + end + + local foreign_key = {} + for _, key in ipairs(foreign_schema.primary_key) do + if entity[foreign_field_name] then + foreign_key[key] = entity[foreign_field_name][key] + + else + if primary_key[key] then + foreign_key[key] = primary_key[key] + + elseif primary_key[foreign_field_name] then + foreign_key[key] = primary_key[foreign_field_name][key] + end + end + end + + return nil, errors:foreign_key_violation_invalid_reference(foreign_key, + foreign_field_name, + foreign_schema.name) + + else + local found, e = find(err, "is still referenced from table", 1, true) + if not found then + return error("could not parse foreign key violation error message: " .. err) + end + + return nil, errors:foreign_key_violation_restricted(schema.name, sub(err, e + 3, -3)) + end + end + + return nil, errors:database_error(err) +end + + +local function get_ttl_value(strategy, attributes, options) + local ttl_value = options and options.ttl + local is_update = options and options.update + local fields = strategy.fields + + if ttl_value == 0 or not ttl_value then + return null + end + + if not is_update and + attributes.created_at and + fields.created_at and + fields.created_at.timestamp and + fields.created_at.auto then + return ttl_value + attributes.created_at + end + + if is_update and + attributes.updated_at and + fields.updated_at and + fields.updated_at.timestamp and + fields.updated_at.auto then + return ttl_value + attributes.updated_at + end + + return ttl_value + now_updated() +end + + +local function get_ws_id() + local phase = get_phase() + if phase ~= "init" and phase ~= "init_worker" then + return ngx.ctx.workspace or kong.default_workspace + end +end + + +local function execute(strategy, statement_name, attributes, options) + local ws_id + local has_ws_id = strategy.schema.workspaceable + if has_ws_id then + if options and options.workspace then + if options.workspace ~= null then + ws_id = options.workspace + end + else + ws_id = get_ws_id() + end + + if not ws_id then + statement_name = statement_name .. "_global" + end + end + + local connector = strategy.connector + local statement = strategy.statements[statement_name] + if not attributes then + return connector:query(statement[1], statement[2]) + end + + local fields = strategy.fields + local argn = statement.argn + local argv = statement.argv + local argc = statement.argc + + clear_tab(argv) + + local is_update = options and options.update + local has_ttl = strategy.schema.ttl + local skip_ttl = options and options.skip_ttl + if has_ws_id then + assert(ws_id == nil or type(ws_id) == "string") + argv[0] = escape_literal(connector, ws_id, "ws_id") + end + + for i = 1, argc do + local name = argn[i] + local value + if has_ttl and name == "ttl" and not skip_ttl then + value = (options and options.ttl) + and get_ttl_value(strategy, attributes, options) + + elseif i == argc and is_update and attributes[UNIQUE] then + value = attributes[UNIQUE] + if type(value) == "table" then + value = value[name] + end + + else + value = attributes[name] + end + + argv[i] = (value == nil and is_update) + and escape_identifier(connector, name) + or escape_literal(connector, value, fields[name]) + end + + local sql = statement.make(argv) + return connector:query(sql, statement.operation) +end + + +local function page(self, size, token, foreign_key, foreign_entity_name, options) + if not size then + size = self.connector:get_page_size(options) + end + + local limit = size + 1 + + local statement_name + local attributes = { + [LIMIT] = limit, + } + + local suffix = token and "_next" or "_first" + if foreign_entity_name then + statement_name = "page_for_" .. foreign_entity_name .. suffix + attributes[foreign_entity_name] = foreign_key + + elseif options and options.tags then + statement_name = options.tags_cond == "or" and + "page_by_tags_or" .. suffix or + "page_by_tags_and" .. suffix + attributes.tags = options.tags + + else + statement_name = "page" .. suffix + end + + if options and options.export then + statement_name = statement_name .. "_for_export" + end + + if token then + local token_decoded = decode_base64(token) + if not token_decoded then + return nil, self.errors:invalid_offset(token, "bad base64 encoding") + end + + token_decoded = cjson_safe.decode(token_decoded) + if not token_decoded then + return nil, self.errors:invalid_offset(token, "bad json encoding") + end + + for i, field_name in ipairs(self.schema.primary_key) do + attributes[field_name] = token_decoded[i] + end + end + + local res, err = execute(self, statement_name, self.collapse(attributes), options) + + if not res then + return toerror(self, err) + end + + local rows = new_tab(size, 0) + + for i = 1, limit do + local row = res[i] + if not row then + break + end + + if i == limit then + row = res[size] + local offset = {} + for _, field_name in ipairs(self.schema.primary_key) do + insert(offset, row[field_name]) + end + + offset = cjson_safe.encode(offset) + offset = encode_base64(offset, true) + + return rows, nil, offset + end + + rows[i] = self.expand(row) + end + + return rows +end + + +local function make_select_for(foreign_entity_name) + return function(self, foreign_key, size, token, options) + return page(self, size, token, foreign_key, foreign_entity_name, options) + end +end + + +local _mt = {} + + +_mt.__index = _mt + + +function _mt:truncate(options) + local res, err = execute(self, "truncate", nil, options) + if not res then + return toerror(self, err) + end + return true, nil +end + + +function _mt:insert(entity, options) + local res, err = execute(self, "insert", self.collapse(entity), options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + + return nil, nil + end + + return toerror(self, err, nil, entity) +end + + +function _mt:select(primary_key, options) + local statement_name = "select" + if self.schema.ttl and options and options.skip_ttl then + statement_name = "select_skip_ttl" + end + + local res, err = execute(self, statement_name, self.collapse(primary_key), options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + + return nil, nil + end + + return toerror(self, err, primary_key) +end + + +function _mt:select_by_field(field_name, unique_value, options) + local statement_name = "select_by_" .. field_name + + if self.schema.ttl and options and options.skip_ttl then + statement_name = statement_name .. "_skip_ttl" + end + + local filter = { + [field_name] = unique_value, + } + + local res, err = execute(self, statement_name, self.collapse(filter), options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + + return nil, nil + end + + return toerror(self, err, filter) +end + + +local function update_options(options) + return { + update = true, + ttl = options and options.ttl, + workspace = options and options.workspace ~= null and options.workspace, + } +end + + +function _mt:update(primary_key, entity, options) + local res, err = execute(self, "update", + self.collapse(primary_key, entity), + update_options(options)) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found(primary_key) + end + + return toerror(self, err, primary_key, entity) +end + + +function _mt:update_by_field(field_name, unique_value, entity, options) + local filter + if type(unique_value) == "table" then + filter = self.collapse({ [field_name] = unique_value }) + else + filter = unique_value + end + + local res, err = execute(self, "update_by_" .. field_name, + self.collapse({ [UNIQUE] = filter }, entity), + update_options(options)) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found_by_field { + [field_name] = unique_value, + } + end + + return toerror(self, err, { [field_name] = unique_value }, entity) +end + + +function _mt:upsert(primary_key, entity, options) + local collapsed_entity = self.collapse(entity, primary_key) + local res, err = execute(self, "upsert", collapsed_entity, options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found(primary_key) + end + + return toerror(self, err, primary_key, entity) +end + + +function _mt:upsert_by_field(field_name, unique_value, entity, options) + local collapsed_entity = self.collapse(entity, { + [field_name] = unique_value + }) + local res, err = execute(self, "upsert_by_" .. field_name, collapsed_entity, options) + if res then + local row = res[1] + if row then + return self.expand(row), nil + end + return nil, self.errors:not_found_by_field { + [field_name] = unique_value, + } + end + + return toerror(self, err, { [field_name] = unique_value }, entity) +end + + +function _mt:delete(primary_key, options) + local statement_name = "delete" + if self.schema.ttl and options and options.skip_ttl then + statement_name = "delete_skip_ttl" + end + local res, err = execute(self, statement_name, self.collapse(primary_key), options) + if res then + if res.affected_rows == 0 then + return nil, nil + end + + return true, nil + end + + return toerror(self, err, primary_key) +end + + +function _mt:delete_by_field(field_name, unique_value, options) + local statement_name = "delete_by_" .. field_name + if self.schema.ttl and options and options.skip_ttl then + statement_name = statement_name .. "_skip_ttl" + end + local filter = { + [field_name] = unique_value, + } + + local res, err = execute(self, statement_name, self.collapse(filter), options) + + if res then + if res.affected_rows == 0 then + return nil, nil + end + + return true, nil + end + + return toerror(self, err, filter) +end + + +function _mt:page(size, token, options) + return page(self, size, token, nil, nil, options) +end + + +function _mt:escape_literal(literal, field_name) + return escape_literal(self.connector, literal, self.fields[field_name]) +end + + +local function format_on_condition(on_condition) + if not on_condition then + return nil + end + on_condition = upper(on_condition) + if on_condition ~= "RESTRICT" and + on_condition ~= "CASCADE" and + on_condition ~= "NULL" and + on_condition ~= "DEFAULT" then + on_condition = nil + end + return on_condition +end + + +local function where_clause(where, ...) + local inputs = pack(...) + return function(add_ws) + local exps = {} + for i = 1, inputs.n do + local exp = inputs[i] + if exp then + if add_ws then + insert(exps, exp) + else + if not exp:match("%(\"ws_id\" = ") then + insert(exps, exp) + end + end + end + end + + if #exps == 0 then + return "" + end + + return where .. concat(exps, "\n" .. rep(" ", #where - 4) .. "AND ") .. "\n" + end +end + + +local function conflict_list(has_ws_id, ...) + local inputs = pack(...) + return function(add_ws) + local exps = inputs + if add_ws and has_ws_id then + insert(exps, 1, '"ws_id"') + end + + return concat(exps, ", ") + end +end + + +-- placeholders in queries must always be constructed as a single string, +-- to avoid ending up genering code like this: `concat { "... $", "2 AND ..." }` +local function placeholder(n) + return "$" .. n +end + + +local _M = {} + + +function _M.new(connector, schema, errors) + local primary_key = schema.primary_key + local primary_key_fields = {} + + for _, field_name in ipairs(primary_key) do + primary_key_fields[field_name] = true + end + + local has_ttl = schema.ttl == true + local has_tags = schema.fields.tags ~= nil + local has_composite_cache_key = schema.cache_key and #schema.cache_key > 1 + local has_ws_id = schema.workspaceable == true + local fields = {} + local fields_hash = {} + + local table_name = schema.table_name + local table_name_escaped = escape_identifier(connector, table_name) + + local foreign_key_list = {} + local foreign_keys = {} + + local unique_fields = {} + + + for field_name, field in schema:each_field() do + if field.transient then + goto continue + end + + if field.type == "foreign" then + local foreign_schema = field.schema + local foreign_key_names = {} + local foreign_key_escaped = {} + local foreign_col_names = {} + local is_unique_foreign = field.unique == true + + for _, foreign_field_name in ipairs(foreign_schema.primary_key) do + local foreign_field + for foreign_schema_field_name, foreign_schema_field in foreign_schema:each_field() do + if foreign_schema_field_name == foreign_field_name then + foreign_field = foreign_schema_field + break + end + end + + local name = field_name .. "_" .. foreign_field_name + + fields_hash[name] = foreign_field + + local prepared_field = { + referenced_table = foreign_schema.name, + referenced_column = foreign_field_name, + on_update = format_on_condition(field.on_update), + on_delete = format_on_condition(field.on_delete), + name = name, + name_escaped = escape_identifier(connector, name), + name_expression = escape_identifier(connector, name, foreign_field), + field_name = field_name, + is_used_in_primary_key = primary_key_fields[field_name] ~= nil, + is_part_of_composite_key = #foreign_schema.primary_key > 1, + is_unique = foreign_field.unique == true, + is_unique_across_ws = foreign_field.unique_across_ws == true, + is_endpoint_key = schema.endpoint_key == field_name, + is_unique_foreign = is_unique_foreign, + } + + if prepared_field.is_used_in_primary_key then + primary_key_fields[field_name] = prepared_field + end + + insert(fields, prepared_field) + + insert(foreign_key_names, name) + insert(foreign_key_escaped, prepared_field.name_escaped) + insert(foreign_col_names, escape_identifier(connector, foreign_field_name)) + insert(foreign_key_list, { + from = name, + entity = field_name, + to = foreign_field_name + }) + end + + foreign_keys[field_name] = { + names = foreign_key_names, + escaped = foreign_key_escaped, + } + + else + fields_hash[field_name] = field + + local is_used_in_primary_key = primary_key_fields[field_name] ~= nil + local is_part_of_composite_key = is_used_in_primary_key and #primary_key > 1 or false + + local prepared_field = { + name = field_name, + name_escaped = escape_identifier(connector, field_name), + name_expression = escape_identifier(connector, field_name, field), + is_used_in_primary_key = is_used_in_primary_key, + is_part_of_composite_key = is_part_of_composite_key, + is_unique = field.unique == true, + is_unique_across_ws = field.unique_across_ws == true, + is_endpoint_key = schema.endpoint_key == field_name, + } + + if prepared_field.is_used_in_primary_key then + primary_key_fields[field_name] = prepared_field + end + + insert(fields, prepared_field) + end + ::continue:: + end + + local primary_key_names = {} + local primary_key_placeholders = {} + local insert_names = {} + local insert_columns = {} + local insert_expressions = {} + local select_expressions = {} + local update_expressions = {} + local update_names = {} + local update_placeholders = {} + local upsert_expressions = {} + local page_next_names = {} + + for i, field in ipairs(fields) do + local name = field.name + local name_escaped = field.name_escaped + local name_expression = field.name_expression + local is_used_in_primary_key = field.is_used_in_primary_key + local is_part_of_composite_key = field.is_part_of_composite_key + local is_unique = field.is_unique + local is_unique_foreign = field.is_unique_foreign + local is_endpoint_key = field.is_endpoint_key + local referenced_table = field.referenced_table + + insert(insert_names, name) + insert(insert_columns, name_escaped) + insert(insert_expressions, "$" .. i) + insert(select_expressions, name_expression) + + if not is_used_in_primary_key then + insert(update_names, name) + insert(update_expressions, name_escaped .. " = $" .. #update_names) + insert(upsert_expressions, name_escaped .. " = " .. "EXCLUDED." .. name_escaped) + end + + if ((not is_used_in_primary_key) or is_part_of_composite_key) + and ((referenced_table and not is_part_of_composite_key) + or is_unique_foreign + or is_unique + or (is_endpoint_key and not is_unique)) + then + -- treat endpoint_key like a unique key anyway, + -- they are indexed (example: target.target) + insert(unique_fields, field) + end + end + + local update_args_names = {} + + for _, update_name in ipairs(update_names) do + insert(update_args_names, update_name) + end + + local cache_key_escaped + if has_composite_cache_key then + cache_key_escaped = escape_identifier(connector, "cache_key") + insert(update_names, "cache_key") + insert(update_args_names, "cache_key") + insert(update_expressions, cache_key_escaped .. " = $" .. #update_names) + insert(upsert_expressions, cache_key_escaped .. " = " .. "EXCLUDED." .. cache_key_escaped) + end + + local ws_id_escaped + if has_ws_id then + ws_id_escaped = escape_identifier(connector, "ws_id") + insert(select_expressions, ws_id_escaped) + insert(update_names, "ws_id") + insert(update_args_names, "ws_id") + insert(update_expressions, ws_id_escaped .. " = $0") + insert(upsert_expressions, ws_id_escaped .. " = " .. "EXCLUDED." .. ws_id_escaped) + end + + local ttl_escaped + if has_ttl then + ttl_escaped = escape_identifier(connector, "ttl") + insert(update_names, "ttl") + insert(update_args_names, "ttl") + insert(update_expressions, ttl_escaped .. " = $" .. #update_names) + insert(upsert_expressions, ttl_escaped .. " = " .. "EXCLUDED." .. ttl_escaped) + end + + local primary_key_escaped = {} + for i, key in ipairs(primary_key) do + local primary_key_field = primary_key_fields[key] + + insert(page_next_names, primary_key_field.name) + insert(primary_key_names, primary_key_field.name) + insert(primary_key_escaped, primary_key_field.name_escaped) + insert(update_args_names, primary_key_field.name) + insert(update_placeholders, "$" .. #update_args_names) + insert(primary_key_placeholders, "$" .. i) + end + + insert(page_next_names, LIMIT) + + local pk_escaped = concat(primary_key_escaped, ", ") + + select_expressions = concat(select_expressions, ", ") + primary_key_placeholders = concat(primary_key_placeholders, ", ") + update_placeholders = concat(update_placeholders, ", ") + + if has_composite_cache_key then + fields_hash.cache_key = { type = "string" } + + insert(insert_names, "cache_key") + insert(insert_expressions, "$" .. #insert_names) + insert(insert_columns, cache_key_escaped) + end + + local ws_id_select_where + if has_ws_id then + fields_hash.ws_id = { type = "string", uuid = true } + + insert(insert_names, "ws_id") + insert(insert_expressions, "$0") + insert(insert_columns, ws_id_escaped) + + ws_id_select_where = "(" .. ws_id_escaped .. " = $0)" + end + + local select_for_export_expressions + local ttl_select_where + if has_ttl then + fields_hash.ttl = { timestamp = true } + + insert(insert_names, "ttl") + insert(insert_expressions, "$" .. #insert_names) + insert(insert_columns, ttl_escaped) + + select_for_export_expressions = concat { + select_expressions, ",", + "FLOOR(EXTRACT(EPOCH FROM (", + ttl_escaped, " AT TIME ZONE 'UTC'", + "))) AS ", ttl_escaped + } + + select_expressions = concat { + select_expressions, ",", + "FLOOR(EXTRACT(EPOCH FROM (", + ttl_escaped, " AT TIME ZONE 'UTC' - CURRENT_TIMESTAMP AT TIME ZONE 'UTC'", + "))) AS ", ttl_escaped + } + + ttl_select_where = concat { + "(", ttl_escaped, " IS NULL OR ", ttl_escaped, " >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC')" + } + end + + insert_expressions = concat(insert_expressions, ", ") + insert_columns = concat(insert_columns, ", ") + + update_expressions = concat(update_expressions, ", ") + + upsert_expressions = concat(upsert_expressions, ", ") + + local primary_key_args = {} + local insert_args = {} + local update_args = {} + local single_args = {} + local page_next_args = {} + + local self = setmetatable({ + connector = connector, + schema = schema, + errors = errors, + expand = #foreign_key_list > 0 and + expand(table_name .. "_expand", foreign_key_list) or + noop, + collapse = collapse(table_name .. "_collapse", foreign_key_list), + fields = fields_hash, + + statements = { + truncate = { + concat { + "TRUNCATE ", table_name_escaped, " RESTART IDENTITY CASCADE;" + }, + "write", + }, + } + }, _mt) + + self.statements["truncate_global"] = self.statements["truncate"] + + local add_statement + local add_statement_for_export + do + local function add(name, opts, add_ws) + local orig_argn = opts.argn + opts = cycle_aware_deep_copy(opts) + + -- ensure LIMIT table is the same + for i, n in ipairs(orig_argn) do + if type(n) == "table" then + opts.argn[i] = n + end + end + + for i = 1, #opts.code do + if type(opts.code[i]) == "function" then + opts.code[i] = opts.code[i](add_ws) + end + opts.code[i] = fmt("%q", opts.code[i]) + end + opts.make = compile(table_name .. "_" .. name, concat(opts.code, ", ")) + opts.code = nil + opts.argc = #opts.argn + self.statements[name] = opts + end + + add_statement = function(name, opts) + add(name .. "_global", opts, false) + add(name, opts, true) + end + + add_statement_for_export = function(name, opts) + add_statement(name, opts) + if has_ttl then + opts.code[2] = select_for_export_expressions + add_statement(name .. "_for_export", opts) + end + end + end + + add_statement("insert", { + operation = "write", + expr = insert_expressions, + cols = insert_columns, + argn = insert_names, + argv = insert_args, + code = { + "INSERT INTO ", table_name_escaped, " (", insert_columns, ")\n", + " VALUES (", insert_expressions, ")\n", + " RETURNING ", select_expressions, ";", + } + }) + + add_statement("upsert", { + operation = "write", + expr = upsert_expressions, + argn = insert_names, + argv = insert_args, + code = { + "INSERT INTO ", table_name_escaped, " (", insert_columns, ")\n", + " VALUES (", insert_expressions, ")\n", + "ON CONFLICT (", pk_escaped, ") DO UPDATE\n", + " SET ", upsert_expressions, "\n", + " RETURNING ", select_expressions, ";", + } + }) + + add_statement("update", { + operation = "write", + expr = update_expressions, + argn = update_args_names, + argv = update_args, + code = { + " UPDATE ", table_name_escaped, "\n", + " SET ", update_expressions, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. update_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "RETURNING ", select_expressions, ";", + } + }) + + add_statement("delete", { + operation = "write", + argn = primary_key_names, + argv = primary_key_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), ";" + } + }) + + add_statement("delete_skip_ttl", { + operation = "write", + argn = primary_key_names, + argv = primary_key_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ws_id_select_where), ";" + } + }) + + add_statement("select", { + operation = "read", + expr = select_expressions, + argn = primary_key_names, + argv = primary_key_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + " LIMIT 1;" + } + }) + + add_statement("select_skip_ttl", { + operation = "read", + expr = select_expressions, + argn = primary_key_names, + argv = primary_key_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") = (" .. primary_key_placeholders .. ")", + ws_id_select_where), + " LIMIT 1;" + } + }) + + add_statement_for_export("page_first", { + operation = "read", + argn = { LIMIT }, + argv = single_args, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT $1;"; + } + }) + + add_statement_for_export("page_next", { + operation = "read", + argn = page_next_names, + argv = page_next_args, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. pk_escaped .. ") > (" .. primary_key_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT " .. placeholder(#page_next_names), ";" + } + }) + + if #foreign_key_list > 0 then + for foreign_entity_name, foreign_key in pairs(foreign_keys) do + local fk_names = foreign_key.names + local fk_escaped = foreign_key.escaped + + local fk_placeholders = {} + local pk_placeholders = {} + + local foreign_key_names = concat(fk_escaped, ", ") + + local argv_first = {} + local argn_first = {} + local argv_next = {} + local argn_next = {} + + for i, fk_name in ipairs(fk_names) do + insert(argn_first, fk_name) + insert(argn_next, fk_name) + insert(fk_placeholders, placeholder(i)) + end + + for i, primary_key_name in ipairs(primary_key_names) do + insert(argn_next, primary_key_name) + insert(pk_placeholders, placeholder(i + #fk_names)) + end + + insert(argn_first, LIMIT) + insert(argn_next, LIMIT) + + fk_placeholders = concat(fk_placeholders, ", ") + pk_placeholders = concat(pk_placeholders, ", ") + + local statement_name = "page_for_" .. foreign_entity_name + + add_statement_for_export(statement_name .. "_first", { + operation = "read", + argn = argn_first, + argv = argv_first, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. foreign_key_names .. ") = (" .. fk_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT ", placeholder(#argn_first), ";"; + } + }) + + add_statement_for_export(statement_name .. "_next", { + operation = "read", + argn = argn_next, + argv = argv_next, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "(" .. foreign_key_names .. ") = (" .. fk_placeholders .. ")", + "(" .. pk_escaped .. ") > (" .. pk_placeholders .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT ", placeholder(#argn_next), ";" + } + }) + + self[statement_name] = make_select_for(foreign_entity_name) + end + end + + if has_tags then + local pk_placeholders = {} + + local argn_first = { "tags", LIMIT } + local argn_next = { "tags" } + + for i, primary_key_name in ipairs(primary_key_names) do + insert(argn_next, primary_key_name) + pk_placeholders[i] = placeholder(i + 1) + end + insert(argn_next, LIMIT) + + for cond, op in pairs({["_and"] = "@>", ["_or"] = "&&"}) do + + add_statement_for_export("page_by_tags" .. cond .. "_first", { + operation = "read", + argn = argn_first, + argv = {}, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "tags " .. op .. " $1", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT $2;"; + }, + }) + + add_statement_for_export("page_by_tags" .. cond .. "_next", { + operation = "read", + argn = argn_next, + argv = {}, + code = { + " SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", "tags " .. op .. " $1", + "(" .. pk_escaped .. ") > (" .. concat(pk_placeholders, ", ") .. ")", + ttl_select_where, + ws_id_select_where), + "ORDER BY ", pk_escaped, "\n", + " LIMIT ", placeholder(#argn_next), ";" + } + }) + end + end + + if has_composite_cache_key then + insert(unique_fields, { + name = "cache_key", + name_escaped = escape_identifier(connector, "cache_key"), + }) + end + + if #unique_fields > 0 then + local update_by_args = {} + + for _, unique_field in ipairs(unique_fields) do + local field_name = unique_field.field_name or unique_field.name + local unique_name = unique_field.name + local unique_escaped = unique_field.name_escaped + local single_names = { unique_name } + + add_statement("select_by_" .. field_name, { + operation = "read", + argn = single_names, + argv = single_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ttl_select_where, + ws_id_select_where), + " LIMIT 1;" + }, + }) + + add_statement("select_by_" .. field_name .. "_skip_ttl", { + operation = "read", + argn = single_names, + argv = single_args, + code = { + "SELECT ", select_expressions, "\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ws_id_select_where), + " LIMIT 1;" + }, + }) + + local update_by_args_names = {} + for _, update_name in ipairs(update_names) do + insert(update_by_args_names, update_name) + end + + insert(update_by_args_names, unique_name) + + add_statement("update_by_" .. field_name, { + operation = "write", + argn = update_by_args_names, + argv = update_by_args, + code = { + " UPDATE ", table_name_escaped, "\n", + " SET ", update_expressions, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $" .. #update_names + 1, + ttl_select_where, + ws_id_select_where), + "RETURNING ", select_expressions, ";", + } + }) + + local conflict_key = unique_escaped + if has_composite_cache_key and not unique_field.is_endpoint_key then + conflict_key = escape_identifier(connector, "cache_key") + end + + local use_ws_id = has_ws_id and not unique_field.is_unique_across_ws + + add_statement("upsert_by_" .. field_name, { + operation = "write", + argn = insert_names, + argv = insert_args, + code = { + "INSERT INTO ", table_name_escaped, " (", insert_columns, ")\n", + " VALUES (", insert_expressions, ")\n", + "ON CONFLICT (", conflict_list(use_ws_id, conflict_key), ") DO UPDATE\n", + " SET ", upsert_expressions, "\n", + " RETURNING ", select_expressions, ";", + } + }) + + add_statement("delete_by_" .. field_name, { + operation = "write", + argn = single_names, + argv = single_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ttl_select_where, + ws_id_select_where), ";" + } + }) + + add_statement("delete_by_" .. field_name .. "_skip_ttl", { + operation = "write", + argn = single_names, + argv = single_args, + code = { + "DELETE\n", + " FROM ", table_name_escaped, "\n", + where_clause( + " WHERE ", unique_escaped .. " = $1", + ws_id_select_where), ";" + } + }) + end + end + + return self +end + + +return _M diff --git a/tests/testdata/lua_kong_check_timer_return/expected.json b/tests/testdata/lua_kong_check_timer_return/expected.json new file mode 100644 index 0000000..d9d724a --- /dev/null +++ b/tests/testdata/lua_kong_check_timer_return/expected.json @@ -0,0 +1,2213 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 3, + 5 + ], + "type": "if_statement", + "start_byte": 11546, + "end_byte": 11586 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 3 + ], + "type": "block", + "start_byte": 11308, + "end_byte": 11601 + }, + "position": 5, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 37, + 3, + 6 + ], + "type": "if_statement", + "start_byte": 11983, + "end_byte": 12023 + }, + "parent": { + "tree": "after", + "path": [ + 37, + 3 + ], + "type": "block", + "start_byte": 11646, + "end_byte": 12038 + }, + "position": 6, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 3, + 4, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 11471, + "end_byte": 11476 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 3, + 4 + ], + "type": "variable_declaration", + "start_byte": 11471, + "end_byte": 11543 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 3, + 6, + 1 + ], + "type": "expression_list", + "start_byte": 11597, + "end_byte": 11601 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 3, + 6 + ], + "type": "return_statement", + "start_byte": 11590, + "end_byte": 11601 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 37, + 3, + 5, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 11915, + "end_byte": 11920 + }, + "parent": { + "tree": "after", + "path": [ + 37, + 3, + 5 + ], + "type": "variable_declaration", + "start_byte": 11915, + "end_byte": 11980 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 37, + 3, + 7, + 1 + ], + "type": "expression_list", + "start_byte": 12034, + "end_byte": 12038 + }, + "parent": { + "tree": "after", + "path": [ + 37, + 3, + 7 + ], + "type": "return_statement", + "start_byte": 12027, + "end_byte": 12038 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 3, + 4, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 11477, + "end_byte": 11484 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 3, + 4, + 1 + ], + "type": "assignment_statement", + "start_byte": 11477, + "end_byte": 11543 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 3, + 4, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 11485, + "end_byte": 11486 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 3, + 4, + 1 + ], + "type": "assignment_statement", + "start_byte": 11477, + "end_byte": 11543 + }, + "position": 1 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 36, + 3, + 4, + 1 + ], + "type": "expression_list", + "start_byte": 11478, + "end_byte": 11534 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 3, + 4, + 1 + ], + "type": "assignment_statement", + "start_byte": 11477, + "end_byte": 11543 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 36, + 3, + 4 + ], + "type": "return_statement", + "start_byte": 11471, + "end_byte": 11534 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 11487, + "dest_end_byte": 11543 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 37, + 3, + 5, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 11921, + "end_byte": 11928 + }, + "parent": { + "tree": "after", + "path": [ + 37, + 3, + 5, + 1 + ], + "type": "assignment_statement", + "start_byte": 11921, + "end_byte": 11980 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 37, + 3, + 5, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 11929, + "end_byte": 11930 + }, + "parent": { + "tree": "after", + "path": [ + 37, + 3, + 5, + 1 + ], + "type": "assignment_statement", + "start_byte": 11921, + "end_byte": 11980 + }, + "position": 1 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 37, + 3, + 5, + 1 + ], + "type": "expression_list", + "start_byte": 11855, + "end_byte": 11904 + }, + "parent": { + "tree": "after", + "path": [ + 37, + 3, + 5, + 1 + ], + "type": "assignment_statement", + "start_byte": 11921, + "end_byte": 11980 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 37, + 3, + 5 + ], + "type": "return_statement", + "start_byte": 11848, + "end_byte": 11904 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 11931, + "dest_end_byte": 11980 + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": 376, + "right_line": 376 + }, + { + "left_line": 377, + "right_line": 377 + }, + { + "left_line": 378, + "right_line": 378 + }, + { + "left_line": 379, + "right_line": 379 + }, + { + "left_line": 380, + "right_line": 380 + }, + { + "left_line": 381, + "right_line": 381 + }, + { + "left_line": 382, + "right_line": 382 + }, + { + "left_line": 383, + "right_line": 383 + }, + { + "left_line": 384, + "right_line": 384 + }, + { + "left_line": 385, + "right_line": 385 + }, + { + "left_line": 386, + "right_line": 386 + }, + { + "left_line": 387, + "right_line": 387 + }, + { + "left_line": 388, + "right_line": 388 + }, + { + "left_line": 389, + "right_line": 389 + }, + { + "left_line": 390, + "right_line": 390 + }, + { + "left_line": 391, + "right_line": 391 + }, + { + "left_line": 392, + "right_line": 392 + }, + { + "left_line": 393, + "right_line": 393 + }, + { + "left_line": 394, + "right_line": 394 + }, + { + "left_line": 395, + "right_line": 395 + }, + { + "left_line": 396, + "right_line": 396 + }, + { + "left_line": 397, + "right_line": 397 + }, + { + "left_line": 398, + "right_line": 398 + }, + { + "left_line": 399, + "right_line": 399 + }, + { + "left_line": 400, + "right_line": 400 + }, + { + "left_line": 401, + "right_line": 401 + }, + { + "left_line": 402, + "right_line": 402 + }, + { + "left_line": 403, + "right_line": 403 + }, + { + "left_line": 404, + "right_line": 404 + }, + { + "left_line": 405, + "right_line": 405 + }, + { + "left_line": 406, + "right_line": 406 + }, + { + "left_line": 407, + "right_line": 407 + }, + { + "left_line": 408, + "right_line": 408 + }, + { + "left_line": 409, + "right_line": 409 + }, + { + "left_line": 410, + "right_line": 410 + }, + { + "left_line": 411, + "right_line": 411 + }, + { + "left_line": 412, + "right_line": 412 + }, + { + "left_line": 413, + "right_line": 413 + }, + { + "left_line": 414, + "right_line": 414 + }, + { + "left_line": 415, + "right_line": 415 + }, + { + "left_line": 416, + "right_line": 416 + }, + { + "left_line": 417, + "right_line": 417 + }, + { + "left_line": 418, + "right_line": 418 + }, + { + "left_line": 419, + "right_line": 419 + }, + { + "left_line": 420, + "right_line": 420 + }, + { + "left_line": 421, + "right_line": 421 + }, + { + "left_line": -1, + "right_line": 422 + }, + { + "left_line": -1, + "right_line": 423 + }, + { + "left_line": -1, + "right_line": 424 + }, + { + "left_line": -1, + "right_line": 425 + }, + { + "left_line": -1, + "right_line": 426 + }, + { + "left_line": 422, + "right_line": 427 + }, + { + "left_line": 423, + "right_line": 428 + }, + { + "left_line": 424, + "right_line": 429 + }, + { + "left_line": 425, + "right_line": 430 + }, + { + "left_line": 426, + "right_line": 431 + }, + { + "left_line": 427, + "right_line": 432 + }, + { + "left_line": 428, + "right_line": 433 + }, + { + "left_line": 429, + "right_line": 434 + }, + { + "left_line": 430, + "right_line": 435 + }, + { + "left_line": 431, + "right_line": 436 + }, + { + "left_line": 432, + "right_line": 437 + }, + { + "left_line": 433, + "right_line": 438 + }, + { + "left_line": 434, + "right_line": 439 + }, + { + "left_line": 435, + "right_line": 440 + }, + { + "left_line": 436, + "right_line": 441 + }, + { + "left_line": 437, + "right_line": 442 + }, + { + "left_line": 438, + "right_line": 443 + }, + { + "left_line": 439, + "right_line": 444 + }, + { + "left_line": 440, + "right_line": 445 + }, + { + "left_line": 441, + "right_line": 446 + }, + { + "left_line": 442, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 447 + }, + { + "left_line": -1, + "right_line": 448 + }, + { + "left_line": -1, + "right_line": 449 + }, + { + "left_line": -1, + "right_line": 450 + }, + { + "left_line": -1, + "right_line": 451 + }, + { + "left_line": -1, + "right_line": 452 + }, + { + "left_line": 443, + "right_line": 453 + }, + { + "left_line": 444, + "right_line": 454 + }, + { + "left_line": 445, + "right_line": 455 + }, + { + "left_line": 446, + "right_line": 456 + }, + { + "left_line": 447, + "right_line": 457 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_check_timer_return/metadata.json b/tests/testdata/lua_kong_check_timer_return/metadata.json new file mode 100644 index 0000000..7b3df6d --- /dev/null +++ b/tests/testdata/lua_kong_check_timer_return/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "bb0a33e53844e490056d1638db56aa689f00164a", + "commit": "bd70457", + "message": "fix(clustering/rpc): check return value of `kong.timer` (#14112)", + "author": "Chrono", + "date": "2025-01-09T10:31:09+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 12, + "deletions": 2 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_check_timer_return/new.lua b/tests/testdata/lua_kong_check_timer_return/new.lua new file mode 100644 index 0000000..b46f796 --- /dev/null +++ b/tests/testdata/lua_kong_check_timer_return/new.lua @@ -0,0 +1,457 @@ +local _M = {} +local _MT = { __index = _M, } + + +local txn = require("resty.lmdb.transaction") +local declarative = require("kong.db.declarative") +local constants = require("kong.constants") +local concurrency = require("kong.concurrency") +local isempty = require("table.isempty") +local events = require("kong.runloop.events") + + +local insert_entity_for_txn = declarative.insert_entity_for_txn +local delete_entity_for_txn = declarative.delete_entity_for_txn +local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY +local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY +local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY +local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS +local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, } +local MAX_RETRY = 5 + + +local assert = assert +local ipairs = ipairs +local fmt = string.format +local ngx_null = ngx.null +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_DEBUG = ngx.DEBUG + + +function _M.new(strategy) + local self = { + strategy = strategy, + } + + return setmetatable(self, _MT) +end + + +local function empty_sync_result() + return { default = { deltas = {}, wipe = false, }, } +end + + +local function full_sync_result() + local deltas, err = declarative.export_config_sync() + if not deltas then + return nil, err + end + + -- wipe dp lmdb, full sync + return { default = { deltas = deltas, wipe = true, }, } +end + + +function _M:init_cp(manager) + local purge_delay = manager.conf.cluster_data_plane_purge_delay + + -- CP + -- Method: kong.sync.v2.get_delta + -- Params: versions: list of current versions of the database + -- example: { default = { version = 1000, }, } + manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions) + ngx_log(ngx_DEBUG, "[kong.sync.v2] config push (connected client)") + + local rpc_peers + if kong.rpc then + rpc_peers = kong.rpc:get_peers() + end + + local default_namespace = current_versions.default + + if not default_namespace then + return nil, "default namespace does not exist inside params" + end + + -- { default = { version = 1000, }, } + local default_namespace_version = default_namespace.version + local node_info = assert(kong.rpc:get_peer_info(node_id)) + + -- follow update_sync_status() in control_plane.lua + local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, { + last_seen = ngx.time(), + hostname = node_id, + ip = node_info.ip, -- get the correct ip + version = node_info.version, -- get from rpc call + labels = node_info.labels, -- get from rpc call + cert_details = node_info.cert_details, -- get from rpc call + sync_status = CLUSTERING_SYNC_STATUS.NORMAL, + config_hash = fmt("%032d", default_namespace_version), + rpc_capabilities = rpc_peers and rpc_peers[node_id] or {}, + }, { ttl = purge_delay, no_broadcast_crud_event = true, }) + if not ok then + ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err) + end + + local latest_version, err = self.strategy:get_latest_version() + if not latest_version then + return nil, err + end + + if default_namespace_version == 0 or + default_namespace_version < latest_version then + return full_sync_result() + end + + return empty_sync_result() + end) +end + + +function _M:init_dp(manager) + local kong_shm = ngx.shared.kong + -- DP + -- Method: kong.sync.v2.notify_new_version + -- Params: new_versions: list of namespaces and their new versions, like: + -- { default = { new_version = 1000, }, } + manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions) + -- TODO: currently only default is supported, and anything else is ignored + local default_new_version = new_versions.default + if not default_new_version then + return nil, "default namespace does not exist inside params" + end + + local version = default_new_version.new_version + if not version then + return nil, "'new_version' key does not exist" + end + + local lmdb_ver = tonumber(declarative.get_current_hash()) or 0 + if lmdb_ver < version then + -- set lastest version to shm + kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version) + return self:sync_once() + end + + return true + end) +end + + +function _M:init(manager, is_cp) + if is_cp then + self:init_cp(manager) + else + self:init_dp(manager) + end +end + + +-- check if rpc connection is ready +local function is_rpc_ready() + -- TODO: find a better way to detect when the RPC layer, including caps list, + -- has been fully initialized, instead of waiting for up to 5.5 seconds + for i = 1, 10 do + local res = kong.rpc:get_peers() + + -- control_plane is ready + if res["control_plane"] then + return true + end + + -- retry later + ngx.sleep(0.1 * i) + end +end + + +local function do_sync() + if not is_rpc_ready() then + return nil, "rpc is not ready" + end + + local msg = { default = + { version = + tonumber(declarative.get_current_hash()) or 0, + }, + } + + local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg) + if not ns_deltas then + ngx_log(ngx_ERR, "sync get_delta error: ", err) + return true + end + + -- ns_deltas should look like: + -- { default = { deltas = { ... }, wipe = true, }, } + + local ns_delta = ns_deltas.default + if not ns_delta then + return nil, "default namespace does not exist inside params" + end + + local deltas = ns_delta.deltas + + if not deltas then + return nil, "sync get_delta error: deltas is null" + end + + if isempty(deltas) then + -- no delta to sync + return true + end + + -- we should find the correct default workspace + -- and replace the old one with it + local default_ws_changed + for _, delta in ipairs(deltas) do + if delta.type == "workspaces" and delta.entity.name == "default" and + kong.default_workspace ~= delta.entity.id + then + kong.default_workspace = delta.entity.id + default_ws_changed = true + break + end + end + assert(type(kong.default_workspace) == "string") + + local t = txn.begin(512) + + local wipe = ns_delta.wipe + if wipe then + t:db_drop(false) + end + + local db = kong.db + + local version = 0 + local opts = {} + local crud_events = {} + local crud_events_n = 0 + + -- delta should look like: + -- { type = ..., entity = { ... }, version = 1, ws_id = ..., } + for _, delta in ipairs(deltas) do + local delta_version = delta.version + local delta_type = delta.type + local delta_entity = delta.entity + local ev + + -- delta should have ws_id to generate the correct lmdb key + -- if entity is workspaceable + -- set the correct workspace for item + opts.workspace = delta.ws_id + + if delta_entity ~= nil and delta_entity ~= ngx_null then + -- upsert the entity + -- does the entity already exists? + local old_entity, err = db[delta_type]:select(delta_entity) + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + local res, err = insert_entity_for_txn(t, delta_type, delta_entity, opts) + if not res then + return nil, err + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] update entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, old_entity and "update" or "create", delta_entity, old_entity, } + end + + else + -- delete the entity, opts for getting correct lmdb key + local old_entity, err = db[delta_type]:select(delta.pk, opts) -- composite key + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] delete entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, "delete", old_entity, } + end + end -- if delta_entity ~= nil and delta_entity ~= ngx_null + + -- wipe the whole lmdb, should not have events + if not wipe then + crud_events_n = crud_events_n + 1 + crud_events[crud_events_n] = ev + end + + -- delta.version should not be nil or ngx.null + assert(type(delta_version) == "number") + + if delta_version ~= version then + version = delta_version + end + end -- for _, delta + + -- store current sync version + t:set(DECLARATIVE_HASH_KEY, fmt("%032d", version)) + + -- store the correct default workspace uuid + if default_ws_changed then + t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace) + end + + local ok, err = t:commit() + if not ok then + return nil, err + end + + if wipe then + kong.core_cache:purge() + kong.cache:purge() + + -- Trigger other workers' callbacks like reconfigure_handler. + -- + -- Full sync could rebuild route, plugins and balancer route, so their + -- hashes are nil. + local reconfigure_data = { kong.default_workspace, nil, nil, nil, } + local ok, err = events.declarative_reconfigure_notify(reconfigure_data) + if not ok then + return nil, err + end + + else + for _, event in ipairs(crud_events) do + -- delta_type, crud_event_type, delta.entity, old_entity + db[event[1]]:post_crud_event(event[2], event[3], event[4]) + end + end + + return true +end + + +local function sync_handler(premature) + if premature then + return + end + + local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync) + if not res and err ~= "timeout" then + ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err) + end +end + + +local sync_once_impl + + +local function start_sync_once_timer(retry_count) + local ok, err = kong.timer:at(0, sync_once_impl, retry_count or 0) + if not ok then + return nil, err + end + + return true +end + + +function sync_once_impl(premature, retry_count) + if premature then + return + end + + sync_handler() + + -- check if "kong.sync.v2.notify_new_version" updates the latest version + + local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY) + if not latest_notified_version then + ngx_log(ngx_DEBUG, "no version notified yet") + return + end + + local current_version = tonumber(declarative.get_current_hash()) or 0 + if current_version >= latest_notified_version then + ngx_log(ngx_DEBUG, "version already updated") + return + end + + -- retry if the version is not updated + retry_count = retry_count or 0 + if retry_count > MAX_RETRY then + ngx_log(ngx_ERR, "sync_once retry count exceeded. retry_count: ", retry_count) + return + end + + return start_sync_once_timer(retry_count + 1) +end + + +function _M:sync_once(delay) + local name = "rpc_sync_v2_once" + local is_managed = kong.timer:is_managed(name) + + -- we are running a sync handler + if is_managed then + return true + end + + local ok, err = kong.timer:named_at(name, delay or 0, sync_once_impl, 0) + if not ok then + return nil, err + end + + return true +end + + +function _M:sync_every(delay, stop) + local name = "rpc_sync_v2_every" + local is_managed = kong.timer:is_managed(name) + + -- we only start or stop once + + if stop then + if is_managed then + assert(kong.timer:cancel(name)) + end + return true + end + + if is_managed then + return true + end + + local ok, err = kong.timer:named_every(name, delay, sync_handler) + if not ok then + return nil, err + end + + return true +end + + +return _M diff --git a/tests/testdata/lua_kong_check_timer_return/old.lua b/tests/testdata/lua_kong_check_timer_return/old.lua new file mode 100644 index 0000000..5af99ca --- /dev/null +++ b/tests/testdata/lua_kong_check_timer_return/old.lua @@ -0,0 +1,447 @@ +local _M = {} +local _MT = { __index = _M, } + + +local txn = require("resty.lmdb.transaction") +local declarative = require("kong.db.declarative") +local constants = require("kong.constants") +local concurrency = require("kong.concurrency") +local isempty = require("table.isempty") +local events = require("kong.runloop.events") + + +local insert_entity_for_txn = declarative.insert_entity_for_txn +local delete_entity_for_txn = declarative.delete_entity_for_txn +local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY +local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY +local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY +local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS +local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, } +local MAX_RETRY = 5 + + +local assert = assert +local ipairs = ipairs +local fmt = string.format +local ngx_null = ngx.null +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_DEBUG = ngx.DEBUG + + +function _M.new(strategy) + local self = { + strategy = strategy, + } + + return setmetatable(self, _MT) +end + + +local function empty_sync_result() + return { default = { deltas = {}, wipe = false, }, } +end + + +local function full_sync_result() + local deltas, err = declarative.export_config_sync() + if not deltas then + return nil, err + end + + -- wipe dp lmdb, full sync + return { default = { deltas = deltas, wipe = true, }, } +end + + +function _M:init_cp(manager) + local purge_delay = manager.conf.cluster_data_plane_purge_delay + + -- CP + -- Method: kong.sync.v2.get_delta + -- Params: versions: list of current versions of the database + -- example: { default = { version = 1000, }, } + manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions) + ngx_log(ngx_DEBUG, "[kong.sync.v2] config push (connected client)") + + local rpc_peers + if kong.rpc then + rpc_peers = kong.rpc:get_peers() + end + + local default_namespace = current_versions.default + + if not default_namespace then + return nil, "default namespace does not exist inside params" + end + + -- { default = { version = 1000, }, } + local default_namespace_version = default_namespace.version + local node_info = assert(kong.rpc:get_peer_info(node_id)) + + -- follow update_sync_status() in control_plane.lua + local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, { + last_seen = ngx.time(), + hostname = node_id, + ip = node_info.ip, -- get the correct ip + version = node_info.version, -- get from rpc call + labels = node_info.labels, -- get from rpc call + cert_details = node_info.cert_details, -- get from rpc call + sync_status = CLUSTERING_SYNC_STATUS.NORMAL, + config_hash = fmt("%032d", default_namespace_version), + rpc_capabilities = rpc_peers and rpc_peers[node_id] or {}, + }, { ttl = purge_delay, no_broadcast_crud_event = true, }) + if not ok then + ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err) + end + + local latest_version, err = self.strategy:get_latest_version() + if not latest_version then + return nil, err + end + + if default_namespace_version == 0 or + default_namespace_version < latest_version then + return full_sync_result() + end + + return empty_sync_result() + end) +end + + +function _M:init_dp(manager) + local kong_shm = ngx.shared.kong + -- DP + -- Method: kong.sync.v2.notify_new_version + -- Params: new_versions: list of namespaces and their new versions, like: + -- { default = { new_version = 1000, }, } + manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions) + -- TODO: currently only default is supported, and anything else is ignored + local default_new_version = new_versions.default + if not default_new_version then + return nil, "default namespace does not exist inside params" + end + + local version = default_new_version.new_version + if not version then + return nil, "'new_version' key does not exist" + end + + local lmdb_ver = tonumber(declarative.get_current_hash()) or 0 + if lmdb_ver < version then + -- set lastest version to shm + kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version) + return self:sync_once() + end + + return true + end) +end + + +function _M:init(manager, is_cp) + if is_cp then + self:init_cp(manager) + else + self:init_dp(manager) + end +end + + +-- check if rpc connection is ready +local function is_rpc_ready() + -- TODO: find a better way to detect when the RPC layer, including caps list, + -- has been fully initialized, instead of waiting for up to 5.5 seconds + for i = 1, 10 do + local res = kong.rpc:get_peers() + + -- control_plane is ready + if res["control_plane"] then + return true + end + + -- retry later + ngx.sleep(0.1 * i) + end +end + + +local function do_sync() + if not is_rpc_ready() then + return nil, "rpc is not ready" + end + + local msg = { default = + { version = + tonumber(declarative.get_current_hash()) or 0, + }, + } + + local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg) + if not ns_deltas then + ngx_log(ngx_ERR, "sync get_delta error: ", err) + return true + end + + -- ns_deltas should look like: + -- { default = { deltas = { ... }, wipe = true, }, } + + local ns_delta = ns_deltas.default + if not ns_delta then + return nil, "default namespace does not exist inside params" + end + + local deltas = ns_delta.deltas + + if not deltas then + return nil, "sync get_delta error: deltas is null" + end + + if isempty(deltas) then + -- no delta to sync + return true + end + + -- we should find the correct default workspace + -- and replace the old one with it + local default_ws_changed + for _, delta in ipairs(deltas) do + if delta.type == "workspaces" and delta.entity.name == "default" and + kong.default_workspace ~= delta.entity.id + then + kong.default_workspace = delta.entity.id + default_ws_changed = true + break + end + end + assert(type(kong.default_workspace) == "string") + + local t = txn.begin(512) + + local wipe = ns_delta.wipe + if wipe then + t:db_drop(false) + end + + local db = kong.db + + local version = 0 + local opts = {} + local crud_events = {} + local crud_events_n = 0 + + -- delta should look like: + -- { type = ..., entity = { ... }, version = 1, ws_id = ..., } + for _, delta in ipairs(deltas) do + local delta_version = delta.version + local delta_type = delta.type + local delta_entity = delta.entity + local ev + + -- delta should have ws_id to generate the correct lmdb key + -- if entity is workspaceable + -- set the correct workspace for item + opts.workspace = delta.ws_id + + if delta_entity ~= nil and delta_entity ~= ngx_null then + -- upsert the entity + -- does the entity already exists? + local old_entity, err = db[delta_type]:select(delta_entity) + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + local res, err = insert_entity_for_txn(t, delta_type, delta_entity, opts) + if not res then + return nil, err + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] update entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, old_entity and "update" or "create", delta_entity, old_entity, } + end + + else + -- delete the entity, opts for getting correct lmdb key + local old_entity, err = db[delta_type]:select(delta.pk, opts) -- composite key + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] delete entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, "delete", old_entity, } + end + end -- if delta_entity ~= nil and delta_entity ~= ngx_null + + -- wipe the whole lmdb, should not have events + if not wipe then + crud_events_n = crud_events_n + 1 + crud_events[crud_events_n] = ev + end + + -- delta.version should not be nil or ngx.null + assert(type(delta_version) == "number") + + if delta_version ~= version then + version = delta_version + end + end -- for _, delta + + -- store current sync version + t:set(DECLARATIVE_HASH_KEY, fmt("%032d", version)) + + -- store the correct default workspace uuid + if default_ws_changed then + t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace) + end + + local ok, err = t:commit() + if not ok then + return nil, err + end + + if wipe then + kong.core_cache:purge() + kong.cache:purge() + + -- Trigger other workers' callbacks like reconfigure_handler. + -- + -- Full sync could rebuild route, plugins and balancer route, so their + -- hashes are nil. + local reconfigure_data = { kong.default_workspace, nil, nil, nil, } + local ok, err = events.declarative_reconfigure_notify(reconfigure_data) + if not ok then + return nil, err + end + + else + for _, event in ipairs(crud_events) do + -- delta_type, crud_event_type, delta.entity, old_entity + db[event[1]]:post_crud_event(event[2], event[3], event[4]) + end + end + + return true +end + + +local function sync_handler(premature) + if premature then + return + end + + local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync) + if not res and err ~= "timeout" then + ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err) + end +end + + +local sync_once_impl + + +local function start_sync_once_timer(retry_count) + local ok, err = kong.timer:at(0, sync_once_impl, retry_count or 0) + if not ok then + return nil, err + end + + return true +end + + +function sync_once_impl(premature, retry_count) + if premature then + return + end + + sync_handler() + + -- check if "kong.sync.v2.notify_new_version" updates the latest version + + local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY) + if not latest_notified_version then + ngx_log(ngx_DEBUG, "no version notified yet") + return + end + + local current_version = tonumber(declarative.get_current_hash()) or 0 + if current_version >= latest_notified_version then + ngx_log(ngx_DEBUG, "version already updated") + return + end + + -- retry if the version is not updated + retry_count = retry_count or 0 + if retry_count > MAX_RETRY then + ngx_log(ngx_ERR, "sync_once retry count exceeded. retry_count: ", retry_count) + return + end + + return start_sync_once_timer(retry_count + 1) +end + + +function _M:sync_once(delay) + local name = "rpc_sync_v2_once" + local is_managed = kong.timer:is_managed(name) + + -- we are running a sync handler + if is_managed then + return true + end + + return kong.timer:named_at(name, delay or 0, sync_once_impl, 0) +end + + +function _M:sync_every(delay, stop) + local name = "rpc_sync_v2_every" + local is_managed = kong.timer:is_managed(name) + + -- we only start or stop once + + if stop then + if is_managed then + assert(kong.timer:cancel(name)) + end + return true + end + + if is_managed then + return true + end + + return kong.timer:named_every(name, delay, sync_handler) +end + + +return _M diff --git a/tests/testdata/lua_kong_comment_docs_cors/expected.json b/tests/testdata/lua_kong_comment_docs_cors/expected.json new file mode 100644 index 0000000..6d6097a --- /dev/null +++ b/tests/testdata/lua_kong_comment_docs_cors/expected.json @@ -0,0 +1,284 @@ +{ + "version": "v1", + "actions": [ + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 4, + 1, + 0, + 1, + 2, + 2, + 0, + 0, + 2, + 1, + 2, + 0, + 0, + 0, + 2, + 0, + 2 + ], + "type": "string", + "label": "\"List of allowed domains for the `Access-Control-Allow-Origin` header. If you want to allow all origins, add `*` as a single value to this configuration field. The accepted values can either be flat strings or PCRE regexes.\"", + "start_byte": 653, + "end_byte": 877 + }, + "old_value": "\"List of allowed domains for the `Access-Control-Allow-Origin` header. If you want to allow all origins, add `*` as a single value to this configuration field. The accepted values can either be flat strings or PCRE regexes.\"", + "new_value": "\"List of allowed domains for the `Access-Control-Allow-Origin` header. If you want to allow all origins, add `*` as a single value to this configuration field. The accepted values can either be flat strings or PCRE regexes. NOTE: If you don't specify any allowed domains, all origins are allowed.\"", + "dest_node": { + "tree": "after", + "path": [ + 4, + 1, + 0, + 1, + 2, + 2, + 0, + 0, + 2, + 1, + 2, + 0, + 0, + 0, + 2, + 0, + 2 + ], + "type": "string", + "label": "\"List of allowed domains for the `Access-Control-Allow-Origin` header. If you want to allow all origins, add `*` as a single value to this configuration field. The accepted values can either be flat strings or PCRE regexes. NOTE: If you don't specify any allowed domains, all origins are allowed.\"", + "start_byte": 653, + "end_byte": 950 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_comment_docs_cors/metadata.json b/tests/testdata/lua_kong_comment_docs_cors/metadata.json new file mode 100644 index 0000000..adc0f3b --- /dev/null +++ b/tests/testdata/lua_kong_comment_docs_cors/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "f73549eb136586414c4326ffa1f369ed60eb4c32", + "commit": "acb37df", + "message": "docs(cors): clarify behavior with no origins specified (#14721)", + "author": "Lucie Milan", + "date": "2025-08-06T05:35:20+02:00", + "language": "lua", + "files_changed": 1, + "insertions": 1, + "deletions": 1 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_comment_docs_cors/new.lua b/tests/testdata/lua_kong_comment_docs_cors/new.lua new file mode 100644 index 0000000..86b102f --- /dev/null +++ b/tests/testdata/lua_kong_comment_docs_cors/new.lua @@ -0,0 +1,54 @@ +local typedefs = require "kong.db.schema.typedefs" +local is_regex = require("kong.db.schema").validators.is_regex + + +local METHODS = { + "GET", + "HEAD", + "PUT", + "PATCH", + "POST", + "DELETE", + "OPTIONS", + "TRACE", + "CONNECT", +} + + +local function validate_asterisk_or_regex(value) + if value == "*" or is_regex(value) then + return true + end + return nil, string.format("'%s' is not a valid regex", tostring(value)) +end + + +return { + name = "cors", + fields = { + { consumer = typedefs.no_consumer }, + { protocols = typedefs.protocols_http }, + { config = { + type = "record", + fields = { + { origins = { description = "List of allowed domains for the `Access-Control-Allow-Origin` header. If you want to allow all origins, add `*` as a single value to this configuration field. The accepted values can either be flat strings or PCRE regexes. NOTE: If you don't specify any allowed domains, all origins are allowed.", type = "array", + elements = { + type = "string", + custom_validator = validate_asterisk_or_regex, + }, }, }, + { headers = { description = "Value for the `Access-Control-Allow-Headers` header.", type = "array", elements = { type = "string" }, }, }, + { exposed_headers = { description = "Value for the `Access-Control-Expose-Headers` header. If not specified, no custom headers are exposed.", type = "array", elements = { type = "string" }, }, }, + { methods = { description = "'Value for the `Access-Control-Allow-Methods` header. Available options include `GET`, `HEAD`, `PUT`, `PATCH`, `POST`, `DELETE`, `OPTIONS`, `TRACE`, `CONNECT`. By default, all options are allowed.'", type = "array", + default = METHODS, + elements = { + type = "string", + one_of = METHODS, + }, }, }, + { max_age = { description = "Indicates how long the results of the preflight request can be cached, in `seconds`.", type = "number" }, }, + { credentials = { description = "Flag to determine whether the `Access-Control-Allow-Credentials` header should be sent with `true` as the value.", type = "boolean", required = true, default = false }, }, + { private_network = { description = "Flag to determine whether the `Access-Control-Allow-Private-Network` header should be sent with `true` as the value.", type = "boolean", required = true, default = false }, }, + { preflight_continue = { description = "A boolean value that instructs the plugin to proxy the `OPTIONS` preflight request to the Upstream service.", type = "boolean", required = true, default = false }, }, + { allow_origin_absent = { description = "A boolean value that skip cors response headers when origin header of request is empty", type = "boolean", required = true, default = true }, }, + }, }, }, + }, +} diff --git a/tests/testdata/lua_kong_comment_docs_cors/old.lua b/tests/testdata/lua_kong_comment_docs_cors/old.lua new file mode 100644 index 0000000..1334f78 --- /dev/null +++ b/tests/testdata/lua_kong_comment_docs_cors/old.lua @@ -0,0 +1,54 @@ +local typedefs = require "kong.db.schema.typedefs" +local is_regex = require("kong.db.schema").validators.is_regex + + +local METHODS = { + "GET", + "HEAD", + "PUT", + "PATCH", + "POST", + "DELETE", + "OPTIONS", + "TRACE", + "CONNECT", +} + + +local function validate_asterisk_or_regex(value) + if value == "*" or is_regex(value) then + return true + end + return nil, string.format("'%s' is not a valid regex", tostring(value)) +end + + +return { + name = "cors", + fields = { + { consumer = typedefs.no_consumer }, + { protocols = typedefs.protocols_http }, + { config = { + type = "record", + fields = { + { origins = { description = "List of allowed domains for the `Access-Control-Allow-Origin` header. If you want to allow all origins, add `*` as a single value to this configuration field. The accepted values can either be flat strings or PCRE regexes.", type = "array", + elements = { + type = "string", + custom_validator = validate_asterisk_or_regex, + }, }, }, + { headers = { description = "Value for the `Access-Control-Allow-Headers` header.", type = "array", elements = { type = "string" }, }, }, + { exposed_headers = { description = "Value for the `Access-Control-Expose-Headers` header. If not specified, no custom headers are exposed.", type = "array", elements = { type = "string" }, }, }, + { methods = { description = "'Value for the `Access-Control-Allow-Methods` header. Available options include `GET`, `HEAD`, `PUT`, `PATCH`, `POST`, `DELETE`, `OPTIONS`, `TRACE`, `CONNECT`. By default, all options are allowed.'", type = "array", + default = METHODS, + elements = { + type = "string", + one_of = METHODS, + }, }, }, + { max_age = { description = "Indicates how long the results of the preflight request can be cached, in `seconds`.", type = "number" }, }, + { credentials = { description = "Flag to determine whether the `Access-Control-Allow-Credentials` header should be sent with `true` as the value.", type = "boolean", required = true, default = false }, }, + { private_network = { description = "Flag to determine whether the `Access-Control-Allow-Private-Network` header should be sent with `true` as the value.", type = "boolean", required = true, default = false }, }, + { preflight_continue = { description = "A boolean value that instructs the plugin to proxy the `OPTIONS` preflight request to the Upstream service.", type = "boolean", required = true, default = false }, }, + { allow_origin_absent = { description = "A boolean value that skip cors response headers when origin header of request is empty", type = "boolean", required = true, default = true }, }, + }, }, }, + }, +} diff --git a/tests/testdata/lua_kong_dp_status_ready_rpc/expected.json b/tests/testdata/lua_kong_dp_status_ready_rpc/expected.json new file mode 100644 index 0000000..c44ea08 --- /dev/null +++ b/tests/testdata/lua_kong_dp_status_ready_rpc/expected.json @@ -0,0 +1,1905 @@ +{ + "version": "v1", + "actions": [ + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 2345, + "end_byte": 2350 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5 + ], + "type": "variable_declaration", + "start_byte": 4697, + "end_byte": 4782 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0 + ], + "type": "variable_declaration", + "start_byte": 2345, + "end_byte": 2401 + }, + "old_position": 0, + "dest_start_byte": 4697, + "dest_end_byte": 4702, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 2351, + "end_byte": 2364 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1 + ], + "type": "assignment_statement", + "start_byte": 4703, + "end_byte": 4782 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1 + ], + "type": "assignment_statement", + "start_byte": 2351, + "end_byte": 2401 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 4703, + "dest_end_byte": 4723, + "group_id": "group-1" + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 2365, + "end_byte": 2366 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 4724, + "end_byte": 4725 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1 + ], + "type": "assignment_statement", + "start_byte": 4703, + "end_byte": 4782 + }, + "position": 1 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 0, + 2 + ], + "type": "identifier", + "label": "pending", + "start_byte": 2388, + "end_byte": 2395 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 2 + ], + "type": "identifier", + "label": "pending", + "start_byte": 4775, + "end_byte": 4782 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4782 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 6 + ], + "type": "function_call", + "start_byte": 4787, + "end_byte": 6328 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 413, + "end_byte": 6328 + }, + "position": 6, + "subtree": true + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 0, + 0 + ], + "type": "identifier", + "label": "skip_rpc_sync", + "start_byte": 2351, + "end_byte": 2364 + }, + "old_value": "skip_rpc_sync", + "new_value": "describe_rpc_sync_on", + "dest_node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 0, + 0 + ], + "type": "identifier", + "label": "describe_rpc_sync_on", + "start_byte": 4703, + "end_byte": 4723 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 2, + 5 + ], + "type": "variable_declaration", + "start_byte": 3126, + "end_byte": 3184 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2326, + "end_byte": 4682 + }, + "position": 5, + "subtree": true + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 2384, + "end_byte": 2387 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4758 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 2367, + "end_byte": 2395 + }, + "old_position": 1, + "dest_start_byte": 4738, + "dest_end_byte": 4741 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "or", + "start_byte": 2396, + "end_byte": 2398 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4771 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0 + ], + "type": "binary_expression", + "start_byte": 2367, + "end_byte": 2401 + }, + "old_position": 1, + "dest_start_byte": 4759, + "dest_end_byte": 4762 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 2 + ], + "type": "identifier", + "label": "it", + "start_byte": 2399, + "end_byte": 2401 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4771 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0 + ], + "type": "binary_expression", + "start_byte": 2367, + "end_byte": 2401 + }, + "old_position": 2, + "dest_start_byte": 4763, + "dest_end_byte": 4771, + "group_id": "group-1" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "or", + "start_byte": 4772, + "end_byte": 4774 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4782 + }, + "position": 1 + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 6, + 0 + ], + "type": "identifier", + "label": "skip_rpc_sync", + "start_byte": 3210, + "end_byte": 3223 + }, + "old_value": "skip_rpc_sync", + "new_value": "it_rpc_sync_off", + "dest_node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 2, + 6, + 0 + ], + "type": "identifier", + "label": "it_rpc_sync_off", + "start_byte": 3191, + "end_byte": 3206 + } + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 2367, + "end_byte": 2383 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4758 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 2367, + "end_byte": 2395 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 4742, + "dest_end_byte": 4758 + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "or", + "start_byte": 2396, + "end_byte": 2398 + }, + "old_value": "or", + "new_value": "and", + "dest_node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 4759, + "end_byte": 4762 + } + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 3, + 0, + 1, + 2, + 0, + 2 + ], + "type": "identifier", + "label": "it", + "start_byte": 2399, + "end_byte": 2401 + }, + "old_value": "it", + "new_value": "describe", + "dest_node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0, + 2 + ], + "type": "identifier", + "label": "describe", + "start_byte": 4763, + "end_byte": 4771 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4737 + }, + "parent": { + "tree": "after", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 5, + 1, + 2, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 4726, + "end_byte": 4758 + }, + "position": 0, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 4, + 3, + 1, + 3, + 0, + 1, + 1, + 2, + 4, + 1, + 1, + 2 + ], + "type": "comment", + "start_byte": 2326, + "end_byte": 2338 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": -1 + }, + { + "left_line": 77, + "right_line": -1 + }, + { + "left_line": 78, + "right_line": -1 + }, + { + "left_line": 79, + "right_line": 76 + }, + { + "left_line": 80, + "right_line": 77 + }, + { + "left_line": 81, + "right_line": 78 + }, + { + "left_line": 82, + "right_line": 79 + }, + { + "left_line": 83, + "right_line": 80 + }, + { + "left_line": 84, + "right_line": 81 + }, + { + "left_line": 85, + "right_line": 82 + }, + { + "left_line": 86, + "right_line": 83 + }, + { + "left_line": 87, + "right_line": 84 + }, + { + "left_line": 88, + "right_line": 85 + }, + { + "left_line": 89, + "right_line": 86 + }, + { + "left_line": 90, + "right_line": 87 + }, + { + "left_line": 91, + "right_line": 88 + }, + { + "left_line": 92, + "right_line": 89 + }, + { + "left_line": 93, + "right_line": 90 + }, + { + "left_line": 94, + "right_line": 91 + }, + { + "left_line": 95, + "right_line": 92 + }, + { + "left_line": 96, + "right_line": 93 + }, + { + "left_line": 97, + "right_line": 94 + }, + { + "left_line": 98, + "right_line": 95 + }, + { + "left_line": 99, + "right_line": 96 + }, + { + "left_line": 100, + "right_line": 97 + }, + { + "left_line": 101, + "right_line": 98 + }, + { + "left_line": 102, + "right_line": 99 + }, + { + "left_line": 103, + "right_line": 100 + }, + { + "left_line": 104, + "right_line": 101 + }, + { + "left_line": 105, + "right_line": 102 + }, + { + "left_line": 106, + "right_line": 103 + }, + { + "left_line": 107, + "right_line": 104 + }, + { + "left_line": 108, + "right_line": 105 + }, + { + "left_line": 109, + "right_line": 106 + }, + { + "left_line": 110, + "right_line": 107 + }, + { + "left_line": 111, + "right_line": 108 + }, + { + "left_line": 112, + "right_line": 109 + }, + { + "left_line": 113, + "right_line": 110 + }, + { + "left_line": 114, + "right_line": 111 + }, + { + "left_line": 115, + "right_line": 112 + }, + { + "left_line": 116, + "right_line": 113 + }, + { + "left_line": 117, + "right_line": 114 + }, + { + "left_line": 118, + "right_line": 115 + }, + { + "left_line": 119, + "right_line": 116 + }, + { + "left_line": 120, + "right_line": 117 + }, + { + "left_line": 121, + "right_line": 118 + }, + { + "left_line": 122, + "right_line": 119 + }, + { + "left_line": 123, + "right_line": 120 + }, + { + "left_line": 124, + "right_line": 121 + }, + { + "left_line": 125, + "right_line": 122 + }, + { + "left_line": 126, + "right_line": 123 + }, + { + "left_line": 127, + "right_line": 124 + }, + { + "left_line": 128, + "right_line": 125 + }, + { + "left_line": 129, + "right_line": 126 + }, + { + "left_line": 130, + "right_line": 127 + }, + { + "left_line": 131, + "right_line": 128 + }, + { + "left_line": 132, + "right_line": 129 + }, + { + "left_line": 133, + "right_line": 130 + }, + { + "left_line": 134, + "right_line": 131 + }, + { + "left_line": 135, + "right_line": 132 + }, + { + "left_line": 136, + "right_line": 133 + }, + { + "left_line": 137, + "right_line": 134 + }, + { + "left_line": 138, + "right_line": 135 + }, + { + "left_line": 139, + "right_line": 136 + }, + { + "left_line": 140, + "right_line": 137 + }, + { + "left_line": 141, + "right_line": 138 + }, + { + "left_line": 142, + "right_line": 139 + }, + { + "left_line": 143, + "right_line": 140 + }, + { + "left_line": 144, + "right_line": 141 + }, + { + "left_line": 145, + "right_line": 142 + }, + { + "left_line": 146, + "right_line": 143 + }, + { + "left_line": 147, + "right_line": 144 + }, + { + "left_line": 148, + "right_line": 145 + }, + { + "left_line": 149, + "right_line": 146 + }, + { + "left_line": 150, + "right_line": 147 + }, + { + "left_line": 151, + "right_line": 148 + }, + { + "left_line": 152, + "right_line": 149 + }, + { + "left_line": 153, + "right_line": 150 + }, + { + "left_line": 154, + "right_line": 151 + }, + { + "left_line": 155, + "right_line": 152 + }, + { + "left_line": 156, + "right_line": 153 + }, + { + "left_line": 157, + "right_line": 154 + }, + { + "left_line": 158, + "right_line": 155 + }, + { + "left_line": 159, + "right_line": 156 + }, + { + "left_line": 160, + "right_line": 157 + }, + { + "left_line": 161, + "right_line": 158 + }, + { + "left_line": 162, + "right_line": -1 + }, + { + "left_line": 163, + "right_line": 159 + }, + { + "left_line": 164, + "right_line": 160 + }, + { + "left_line": 165, + "right_line": 161 + }, + { + "left_line": -1, + "right_line": 162 + }, + { + "left_line": -1, + "right_line": 163 + }, + { + "left_line": -1, + "right_line": 164 + }, + { + "left_line": -1, + "right_line": 165 + }, + { + "left_line": -1, + "right_line": 166 + }, + { + "left_line": -1, + "right_line": 167 + }, + { + "left_line": -1, + "right_line": 168 + }, + { + "left_line": -1, + "right_line": 169 + }, + { + "left_line": -1, + "right_line": 170 + }, + { + "left_line": -1, + "right_line": 171 + }, + { + "left_line": -1, + "right_line": 172 + }, + { + "left_line": -1, + "right_line": 173 + }, + { + "left_line": -1, + "right_line": 174 + }, + { + "left_line": -1, + "right_line": 175 + }, + { + "left_line": -1, + "right_line": 176 + }, + { + "left_line": -1, + "right_line": 177 + }, + { + "left_line": -1, + "right_line": 178 + }, + { + "left_line": -1, + "right_line": 179 + }, + { + "left_line": -1, + "right_line": 180 + }, + { + "left_line": -1, + "right_line": 181 + }, + { + "left_line": -1, + "right_line": 182 + }, + { + "left_line": -1, + "right_line": 183 + }, + { + "left_line": -1, + "right_line": 184 + }, + { + "left_line": -1, + "right_line": 185 + }, + { + "left_line": -1, + "right_line": 186 + }, + { + "left_line": -1, + "right_line": 187 + }, + { + "left_line": -1, + "right_line": 188 + }, + { + "left_line": -1, + "right_line": 189 + }, + { + "left_line": -1, + "right_line": 190 + }, + { + "left_line": -1, + "right_line": 191 + }, + { + "left_line": -1, + "right_line": 192 + }, + { + "left_line": -1, + "right_line": 193 + }, + { + "left_line": -1, + "right_line": 194 + }, + { + "left_line": -1, + "right_line": 195 + }, + { + "left_line": -1, + "right_line": 196 + }, + { + "left_line": -1, + "right_line": 197 + }, + { + "left_line": -1, + "right_line": 198 + }, + { + "left_line": -1, + "right_line": 199 + }, + { + "left_line": -1, + "right_line": 200 + }, + { + "left_line": -1, + "right_line": 201 + }, + { + "left_line": -1, + "right_line": 202 + }, + { + "left_line": -1, + "right_line": 203 + }, + { + "left_line": -1, + "right_line": 204 + }, + { + "left_line": -1, + "right_line": 205 + }, + { + "left_line": -1, + "right_line": 206 + }, + { + "left_line": -1, + "right_line": 207 + }, + { + "left_line": -1, + "right_line": 208 + }, + { + "left_line": -1, + "right_line": 209 + }, + { + "left_line": -1, + "right_line": 210 + }, + { + "left_line": -1, + "right_line": 211 + }, + { + "left_line": 166, + "right_line": 212 + }, + { + "left_line": 167, + "right_line": 213 + }, + { + "left_line": 168, + "right_line": 214 + }, + { + "left_line": 169, + "right_line": 215 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_dp_status_ready_rpc/metadata.json b/tests/testdata/lua_kong_dp_status_ready_rpc/metadata.json new file mode 100644 index 0000000..6ae3815 --- /dev/null +++ b/tests/testdata/lua_kong_dp_status_ready_rpc/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "145816122ec8066c03502a794617721dc8058b15", + "commit": "51cb5f1", + "message": "tests(clustering): dp status ready when use RPC Sync (#14035)", + "author": "hanjian", + "date": "2024-12-23T14:23:26+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 52, + "deletions": 6 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_dp_status_ready_rpc/new.lua b/tests/testdata/lua_kong_dp_status_ready_rpc/new.lua new file mode 100644 index 0000000..7db58b8 --- /dev/null +++ b/tests/testdata/lua_kong_dp_status_ready_rpc/new.lua @@ -0,0 +1,215 @@ +-- 09-hybrid_mode/11-status_ready.lua +local helpers = require "spec.helpers" + +local cp_status_port = helpers.get_available_port() +local dp_status_port = 8100 + +for _, v in ipairs({ {"off", "off"}, {"on", "off"}, {"on", "on"}, }) do + local rpc, rpc_sync = v[1], v[2] + +for _, strategy in helpers.each_strategy() do + + describe("Hybrid Mode - status ready #" .. strategy .. " rpc_sync=" .. rpc_sync, function() + + helpers.get_db_utils(strategy, {}) + + local function start_kong_dp() + return helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = "serve_dp", + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + proxy_listen = "127.0.0.1:9002", + nginx_main_worker_processes = 8, + status_listen = "127.0.0.1:" .. dp_status_port, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + }) + end + + local function start_kong_cp() + return helpers.start_kong({ + role = "control_plane", + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + database = strategy, + prefix = "serve_cp", + cluster_listen = "127.0.0.1:9005", + nginx_conf = "spec/fixtures/custom_nginx.template", + status_listen = "127.0.0.1:" .. cp_status_port, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + }) + end + + describe("dp should returns 503 without cp", function() + + lazy_setup(function() + assert(start_kong_dp()) + end) + + lazy_teardown(function() + assert(helpers.stop_kong("serve_dp")) + end) + + -- now dp should be not ready + it("should return 503 on data plane", function() + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 503 then + return true + end + end, 10) + end) + end) + + describe("dp status ready endpoint for no config", function() + lazy_setup(function() + assert(start_kong_cp()) + assert(start_kong_dp()) + end) + + lazy_teardown(function() + assert(helpers.stop_kong("serve_cp")) + assert(helpers.stop_kong("serve_dp")) + end) + + -- now cp should be ready + + it("returns 200 on control plane", function() + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', cp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + end) + + -- now dp receive config from cp, so dp should be ready + local it_rpc_sync_off= rpc_sync == "off" and it or pending + it_rpc_sync_off("should return 200 on data plane after configuring", function() + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + + assert(helpers.stop_kong("serve_cp", nil, nil, "QUIT", false)) + + -- DP should keep return 200 after CP is shut down + helpers.wait_until(function() + + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + + -- recovery state between tests + assert(start_kong_cp()) + + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + end) + end) + + local describe_rpc_sync_on = rpc == "on" and rpc_sync == "on" and describe or pending + describe_rpc_sync_on("dp status ready when rpc_sync == on", function() + lazy_setup(function() + assert(start_kong_cp()) + assert(start_kong_dp()) + end) + + lazy_teardown(function() + assert(helpers.stop_kong("serve_cp")) + assert(helpers.stop_kong("serve_dp")) + end) + + it("should return 200 on data plane after configuring when rpc_sync == on", function() + -- insert one entity to make dp ready for incremental sync + + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + http_client:close() + assert.equal(503, res.status) + + local admin_client = helpers.admin_client(10000) + local res = assert(admin_client:post("/services", { + body = { name = "service-001", url = "https://127.0.0.1:15556/request", }, + headers = {["Content-Type"] = "application/json"} + })) + assert.res_status(201, res) + + admin_client:close() + + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + + if status == 200 then + return true + end + end, 10) + end) + end) + end) +end -- for _, strategy +end -- for rpc_sync diff --git a/tests/testdata/lua_kong_dp_status_ready_rpc/old.lua b/tests/testdata/lua_kong_dp_status_ready_rpc/old.lua new file mode 100644 index 0000000..86cd894 --- /dev/null +++ b/tests/testdata/lua_kong_dp_status_ready_rpc/old.lua @@ -0,0 +1,169 @@ +-- 09-hybrid_mode/11-status_ready.lua +local helpers = require "spec.helpers" + +local cp_status_port = helpers.get_available_port() +local dp_status_port = 8100 + +for _, v in ipairs({ {"off", "off"}, {"on", "off"}, {"on", "on"}, }) do + local rpc, rpc_sync = v[1], v[2] + +for _, strategy in helpers.each_strategy() do + + describe("Hybrid Mode - status ready #" .. strategy .. " rpc_sync=" .. rpc_sync, function() + + helpers.get_db_utils(strategy, {}) + + local function start_kong_dp() + return helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = "serve_dp", + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + proxy_listen = "127.0.0.1:9002", + nginx_main_worker_processes = 8, + status_listen = "127.0.0.1:" .. dp_status_port, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + }) + end + + local function start_kong_cp() + return helpers.start_kong({ + role = "control_plane", + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + database = strategy, + prefix = "serve_cp", + cluster_listen = "127.0.0.1:9005", + nginx_conf = "spec/fixtures/custom_nginx.template", + status_listen = "127.0.0.1:" .. cp_status_port, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + }) + end + + describe("dp should returns 503 without cp", function() + + lazy_setup(function() + assert(start_kong_dp()) + end) + + lazy_teardown(function() + assert(helpers.stop_kong("serve_dp")) + end) + + -- now dp should be not ready + it("should return 503 on data plane", function() + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 503 then + return true + end + end, 10) + end) + end) + + describe("dp status ready endpoint for no config", function() + -- XXX FIXME + local skip_rpc_sync = rpc_sync == "on" and pending or it + + lazy_setup(function() + assert(start_kong_cp()) + assert(start_kong_dp()) + end) + + lazy_teardown(function() + assert(helpers.stop_kong("serve_cp")) + assert(helpers.stop_kong("serve_dp")) + end) + + -- now cp should be ready + + it("returns 200 on control plane", function() + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', cp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + end) + + -- now dp receive config from cp, so dp should be ready + + skip_rpc_sync("should return 200 on data plane after configuring", function() + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + + assert(helpers.stop_kong("serve_cp", nil, nil, "QUIT", false)) + + -- DP should keep return 200 after CP is shut down + helpers.wait_until(function() + + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + + -- recovery state between tests + assert(start_kong_cp()) + + helpers.wait_until(function() + local http_client = helpers.http_client('127.0.0.1', dp_status_port) + + local res = http_client:send({ + method = "GET", + path = "/status/ready", + }) + + local status = res and res.status + http_client:close() + if status == 200 then + return true + end + end, 10) + + end) + end) + + end) +end -- for _, strategy +end -- for rpc_sync diff --git a/tests/testdata/lua_kong_enable_rpc_sync_tests/expected.json b/tests/testdata/lua_kong_enable_rpc_sync_tests/expected.json new file mode 100644 index 0000000..d1c9ed4 --- /dev/null +++ b/tests/testdata/lua_kong_enable_rpc_sync_tests/expected.json @@ -0,0 +1,2141 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 0 + ], + "type": "variable_declaration", + "start_byte": 1919, + "end_byte": 1951 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3 + ], + "type": "block", + "start_byte": 1919, + "end_byte": 11333 + }, + "position": 0, + "subtree": true + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 13, + 1, + 0, + 1 + ], + "type": "identifier", + "label": "rpc_sync", + "start_byte": 1888, + "end_byte": 1896 + }, + "old_value": "rpc_sync", + "new_value": "v", + "dest_node": { + "tree": "after", + "path": [ + 12, + 1, + 0, + 1 + ], + "type": "identifier", + "label": "v", + "start_byte": 1852, + "end_byte": 1853 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0, + 1 + ], + "type": "field", + "start_byte": 1882, + "end_byte": 1895 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 1864, + "end_byte": 1912 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0, + 2 + ], + "type": "field", + "start_byte": 1897, + "end_byte": 1909 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 1864, + "end_byte": 1912 + }, + "position": 2, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 8 + ], + "type": "comment", + "start_byte": 8590, + "end_byte": 8671 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2036, + "end_byte": 11328 + }, + "position": 8 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 9 + ], + "type": "variable_declaration", + "start_byte": 8674, + "end_byte": 8735 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2036, + "end_byte": 11328 + }, + "position": 9, + "subtree": true + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 13, + 1, + 2, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 1907, + "end_byte": 1917 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0, + 0 + ], + "type": "field", + "start_byte": 1866, + "end_byte": 1880 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 13, + 1, + 2, + 0, + 1 + ], + "type": "arguments", + "start_byte": 1907, + "end_byte": 1917 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 1866, + "dest_end_byte": 1880 + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 13, + 3, + 0, + 1, + 1, + 2, + 8, + 0 + ], + "type": "identifier", + "label": "describe", + "start_byte": 8480, + "end_byte": 8488 + }, + "old_value": "describe", + "new_value": "only_sync_v1", + "dest_node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 10, + 0 + ], + "type": "identifier", + "label": "only_sync_v1", + "start_byte": 8739, + "end_byte": 8751 + } + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 13, + 3, + 0, + 1, + 1, + 2, + 9, + 0 + ], + "type": "identifier", + "label": "describe", + "start_byte": 9615, + "end_byte": 9623 + }, + "old_value": "describe", + "new_value": "only_sync_v1", + "dest_node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 11, + 0 + ], + "type": "identifier", + "label": "only_sync_v1", + "start_byte": 9920, + "end_byte": 9932 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0, + 0, + 0, + 1 + ], + "type": "field", + "start_byte": 1874, + "end_byte": 1879 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 1, + 2, + 0, + 1, + 0, + 0, + 0 + ], + "type": "table_constructor", + "start_byte": 1866, + "end_byte": 1880 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 5, + 1, + 0, + 2, + 6, + 1, + 0, + 1, + 0, + 13 + ], + "type": "field", + "start_byte": 3347, + "end_byte": 3372 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 5, + 1, + 0, + 2, + 6, + 1, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 2740, + "end_byte": 3417 + }, + "position": 13, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 0, + 14 + ], + "type": "field", + "start_byte": 4847, + "end_byte": 4874 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 4157, + "end_byte": 4925 + }, + "position": 14, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 10, + 1, + 1, + 2, + 1, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 0, + 10 + ], + "type": "field", + "start_byte": 9453, + "end_byte": 9480 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 10, + 1, + 1, + 2, + 1, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 8937, + "end_byte": 9531 + }, + "position": 10, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 11, + 1, + 1, + 2, + 2, + 1, + 0, + 2, + 3, + 1, + 0, + 1, + 0, + 13 + ], + "type": "field", + "start_byte": 10831, + "end_byte": 10858 + }, + "parent": { + "tree": "after", + "path": [ + 12, + 3, + 1, + 1, + 1, + 2, + 11, + 1, + 1, + 2, + 2, + 1, + 0, + 2, + 3, + 1, + 0, + 1, + 0 + ], + "type": "table_constructor", + "start_byte": 10189, + "end_byte": 10909 + }, + "position": 13, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 12 + ], + "type": "comment", + "start_byte": 1845, + "end_byte": 1880 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": -1 + }, + { + "left_line": 75, + "right_line": 74 + }, + { + "left_line": -1, + "right_line": 75 + }, + { + "left_line": -1, + "right_line": 76 + }, + { + "left_line": 76, + "right_line": 77 + }, + { + "left_line": 77, + "right_line": 78 + }, + { + "left_line": 78, + "right_line": 79 + }, + { + "left_line": 79, + "right_line": 80 + }, + { + "left_line": 80, + "right_line": 81 + }, + { + "left_line": 81, + "right_line": 82 + }, + { + "left_line": 82, + "right_line": 83 + }, + { + "left_line": 83, + "right_line": 84 + }, + { + "left_line": 84, + "right_line": 85 + }, + { + "left_line": 85, + "right_line": 86 + }, + { + "left_line": 86, + "right_line": 87 + }, + { + "left_line": 87, + "right_line": 88 + }, + { + "left_line": 88, + "right_line": 89 + }, + { + "left_line": 89, + "right_line": 90 + }, + { + "left_line": 90, + "right_line": 91 + }, + { + "left_line": 91, + "right_line": 92 + }, + { + "left_line": 92, + "right_line": 93 + }, + { + "left_line": 93, + "right_line": 94 + }, + { + "left_line": 94, + "right_line": 95 + }, + { + "left_line": 95, + "right_line": 96 + }, + { + "left_line": 96, + "right_line": 97 + }, + { + "left_line": 97, + "right_line": 98 + }, + { + "left_line": 98, + "right_line": 99 + }, + { + "left_line": 99, + "right_line": 100 + }, + { + "left_line": 100, + "right_line": 101 + }, + { + "left_line": 101, + "right_line": 102 + }, + { + "left_line": 102, + "right_line": 103 + }, + { + "left_line": 103, + "right_line": 104 + }, + { + "left_line": 104, + "right_line": 105 + }, + { + "left_line": 105, + "right_line": 106 + }, + { + "left_line": 106, + "right_line": 107 + }, + { + "left_line": 107, + "right_line": 108 + }, + { + "left_line": 108, + "right_line": 109 + }, + { + "left_line": 109, + "right_line": 110 + }, + { + "left_line": 110, + "right_line": 111 + }, + { + "left_line": 111, + "right_line": 112 + }, + { + "left_line": 112, + "right_line": 113 + }, + { + "left_line": 113, + "right_line": 114 + }, + { + "left_line": 114, + "right_line": 115 + }, + { + "left_line": 115, + "right_line": 116 + }, + { + "left_line": 116, + "right_line": 117 + }, + { + "left_line": -1, + "right_line": 118 + }, + { + "left_line": 117, + "right_line": 119 + }, + { + "left_line": 118, + "right_line": 120 + }, + { + "left_line": 119, + "right_line": 121 + }, + { + "left_line": 120, + "right_line": 122 + }, + { + "left_line": 121, + "right_line": 123 + }, + { + "left_line": 122, + "right_line": 124 + }, + { + "left_line": 123, + "right_line": 125 + }, + { + "left_line": 124, + "right_line": 126 + }, + { + "left_line": 125, + "right_line": 127 + }, + { + "left_line": 126, + "right_line": 128 + }, + { + "left_line": 127, + "right_line": 129 + }, + { + "left_line": 128, + "right_line": 130 + }, + { + "left_line": 129, + "right_line": 131 + }, + { + "left_line": 130, + "right_line": 132 + }, + { + "left_line": 131, + "right_line": 133 + }, + { + "left_line": 132, + "right_line": 134 + }, + { + "left_line": 133, + "right_line": 135 + }, + { + "left_line": 134, + "right_line": 136 + }, + { + "left_line": 135, + "right_line": 137 + }, + { + "left_line": 136, + "right_line": 138 + }, + { + "left_line": 137, + "right_line": 139 + }, + { + "left_line": 138, + "right_line": 140 + }, + { + "left_line": 139, + "right_line": 141 + }, + { + "left_line": 140, + "right_line": 142 + }, + { + "left_line": 141, + "right_line": 143 + }, + { + "left_line": 142, + "right_line": 144 + }, + { + "left_line": 143, + "right_line": 145 + }, + { + "left_line": 144, + "right_line": 146 + }, + { + "left_line": 145, + "right_line": 147 + }, + { + "left_line": 146, + "right_line": 148 + }, + { + "left_line": 147, + "right_line": 149 + }, + { + "left_line": 148, + "right_line": 150 + }, + { + "left_line": 149, + "right_line": 151 + }, + { + "left_line": 150, + "right_line": 152 + }, + { + "left_line": 151, + "right_line": 153 + }, + { + "left_line": 152, + "right_line": 154 + }, + { + "left_line": 153, + "right_line": 155 + }, + { + "left_line": 154, + "right_line": 156 + }, + { + "left_line": 155, + "right_line": 157 + }, + { + "left_line": 156, + "right_line": 158 + }, + { + "left_line": -1, + "right_line": 159 + }, + { + "left_line": 157, + "right_line": 160 + }, + { + "left_line": 158, + "right_line": 161 + }, + { + "left_line": 159, + "right_line": 162 + }, + { + "left_line": 160, + "right_line": 163 + }, + { + "left_line": 161, + "right_line": 164 + }, + { + "left_line": 162, + "right_line": 165 + }, + { + "left_line": 163, + "right_line": 166 + }, + { + "left_line": 164, + "right_line": 167 + }, + { + "left_line": 165, + "right_line": 168 + }, + { + "left_line": 166, + "right_line": 169 + }, + { + "left_line": 167, + "right_line": 170 + }, + { + "left_line": 168, + "right_line": 171 + }, + { + "left_line": 169, + "right_line": 172 + }, + { + "left_line": 170, + "right_line": 173 + }, + { + "left_line": 171, + "right_line": 174 + }, + { + "left_line": 172, + "right_line": 175 + }, + { + "left_line": 173, + "right_line": 176 + }, + { + "left_line": 174, + "right_line": 177 + }, + { + "left_line": 175, + "right_line": 178 + }, + { + "left_line": 176, + "right_line": 179 + }, + { + "left_line": 177, + "right_line": 180 + }, + { + "left_line": 178, + "right_line": 181 + }, + { + "left_line": 179, + "right_line": 182 + }, + { + "left_line": 180, + "right_line": 183 + }, + { + "left_line": 181, + "right_line": 184 + }, + { + "left_line": 182, + "right_line": 185 + }, + { + "left_line": 183, + "right_line": 186 + }, + { + "left_line": 184, + "right_line": 187 + }, + { + "left_line": 185, + "right_line": 188 + }, + { + "left_line": 186, + "right_line": 189 + }, + { + "left_line": 187, + "right_line": 190 + }, + { + "left_line": 188, + "right_line": 191 + }, + { + "left_line": 189, + "right_line": 192 + }, + { + "left_line": 190, + "right_line": 193 + }, + { + "left_line": 191, + "right_line": 194 + }, + { + "left_line": 192, + "right_line": 195 + }, + { + "left_line": 193, + "right_line": 196 + }, + { + "left_line": 194, + "right_line": 197 + }, + { + "left_line": 195, + "right_line": 198 + }, + { + "left_line": 196, + "right_line": 199 + }, + { + "left_line": 197, + "right_line": 200 + }, + { + "left_line": 198, + "right_line": 201 + }, + { + "left_line": 199, + "right_line": 202 + }, + { + "left_line": 200, + "right_line": 203 + }, + { + "left_line": 201, + "right_line": 204 + }, + { + "left_line": 202, + "right_line": 205 + }, + { + "left_line": 203, + "right_line": 206 + }, + { + "left_line": 204, + "right_line": 207 + }, + { + "left_line": 205, + "right_line": 208 + }, + { + "left_line": 206, + "right_line": 209 + }, + { + "left_line": 207, + "right_line": 210 + }, + { + "left_line": 208, + "right_line": 211 + }, + { + "left_line": 209, + "right_line": 212 + }, + { + "left_line": 210, + "right_line": 213 + }, + { + "left_line": 211, + "right_line": 214 + }, + { + "left_line": 212, + "right_line": 215 + }, + { + "left_line": 213, + "right_line": 216 + }, + { + "left_line": 214, + "right_line": 217 + }, + { + "left_line": 215, + "right_line": 218 + }, + { + "left_line": 216, + "right_line": 219 + }, + { + "left_line": 217, + "right_line": 220 + }, + { + "left_line": 218, + "right_line": 221 + }, + { + "left_line": 219, + "right_line": 222 + }, + { + "left_line": 220, + "right_line": 223 + }, + { + "left_line": 221, + "right_line": 224 + }, + { + "left_line": 222, + "right_line": 225 + }, + { + "left_line": 223, + "right_line": 226 + }, + { + "left_line": 224, + "right_line": 227 + }, + { + "left_line": 225, + "right_line": 228 + }, + { + "left_line": 226, + "right_line": 229 + }, + { + "left_line": 227, + "right_line": 230 + }, + { + "left_line": 228, + "right_line": 231 + }, + { + "left_line": 229, + "right_line": 232 + }, + { + "left_line": 230, + "right_line": 233 + }, + { + "left_line": 231, + "right_line": 234 + }, + { + "left_line": 232, + "right_line": 235 + }, + { + "left_line": 233, + "right_line": 236 + }, + { + "left_line": 234, + "right_line": 237 + }, + { + "left_line": 235, + "right_line": 238 + }, + { + "left_line": 236, + "right_line": 239 + }, + { + "left_line": 237, + "right_line": 240 + }, + { + "left_line": 238, + "right_line": 241 + }, + { + "left_line": 239, + "right_line": 242 + }, + { + "left_line": 240, + "right_line": 243 + }, + { + "left_line": 241, + "right_line": 244 + }, + { + "left_line": 242, + "right_line": 245 + }, + { + "left_line": 243, + "right_line": 246 + }, + { + "left_line": 244, + "right_line": 247 + }, + { + "left_line": 245, + "right_line": 248 + }, + { + "left_line": 246, + "right_line": 249 + }, + { + "left_line": 247, + "right_line": 250 + }, + { + "left_line": 248, + "right_line": 251 + }, + { + "left_line": 249, + "right_line": 252 + }, + { + "left_line": 250, + "right_line": 253 + }, + { + "left_line": 251, + "right_line": 254 + }, + { + "left_line": 252, + "right_line": 255 + }, + { + "left_line": 253, + "right_line": 256 + }, + { + "left_line": 254, + "right_line": 257 + }, + { + "left_line": 255, + "right_line": 258 + }, + { + "left_line": 256, + "right_line": 259 + }, + { + "left_line": 257, + "right_line": 260 + }, + { + "left_line": 258, + "right_line": 261 + }, + { + "left_line": 259, + "right_line": 262 + }, + { + "left_line": 260, + "right_line": 263 + }, + { + "left_line": 261, + "right_line": 264 + }, + { + "left_line": 262, + "right_line": 265 + }, + { + "left_line": 263, + "right_line": 266 + }, + { + "left_line": 264, + "right_line": 267 + }, + { + "left_line": 265, + "right_line": 268 + }, + { + "left_line": 266, + "right_line": 269 + }, + { + "left_line": 267, + "right_line": 270 + }, + { + "left_line": 268, + "right_line": 271 + }, + { + "left_line": 269, + "right_line": 272 + }, + { + "left_line": 270, + "right_line": 273 + }, + { + "left_line": 271, + "right_line": 274 + }, + { + "left_line": 272, + "right_line": 275 + }, + { + "left_line": 273, + "right_line": 276 + }, + { + "left_line": 274, + "right_line": 277 + }, + { + "left_line": 275, + "right_line": 278 + }, + { + "left_line": 276, + "right_line": 279 + }, + { + "left_line": 277, + "right_line": 280 + }, + { + "left_line": 278, + "right_line": 281 + }, + { + "left_line": 279, + "right_line": 282 + }, + { + "left_line": 280, + "right_line": 283 + }, + { + "left_line": 281, + "right_line": 284 + }, + { + "left_line": 282, + "right_line": 285 + }, + { + "left_line": 283, + "right_line": 286 + }, + { + "left_line": 284, + "right_line": 287 + }, + { + "left_line": 285, + "right_line": 288 + }, + { + "left_line": 286, + "right_line": 289 + }, + { + "left_line": 287, + "right_line": 290 + }, + { + "left_line": 288, + "right_line": 291 + }, + { + "left_line": 289, + "right_line": 292 + }, + { + "left_line": 290, + "right_line": 293 + }, + { + "left_line": 291, + "right_line": 294 + }, + { + "left_line": 292, + "right_line": 295 + }, + { + "left_line": 293, + "right_line": 296 + }, + { + "left_line": 294, + "right_line": 297 + }, + { + "left_line": -1, + "right_line": 298 + }, + { + "left_line": -1, + "right_line": 299 + }, + { + "left_line": -1, + "right_line": 300 + }, + { + "left_line": 295, + "right_line": 301 + }, + { + "left_line": 296, + "right_line": 302 + }, + { + "left_line": 297, + "right_line": 303 + }, + { + "left_line": 298, + "right_line": 304 + }, + { + "left_line": 299, + "right_line": 305 + }, + { + "left_line": 300, + "right_line": 306 + }, + { + "left_line": 301, + "right_line": 307 + }, + { + "left_line": 302, + "right_line": 308 + }, + { + "left_line": 303, + "right_line": 309 + }, + { + "left_line": 304, + "right_line": 310 + }, + { + "left_line": 305, + "right_line": 311 + }, + { + "left_line": 306, + "right_line": 312 + }, + { + "left_line": 307, + "right_line": 313 + }, + { + "left_line": 308, + "right_line": 314 + }, + { + "left_line": 309, + "right_line": 315 + }, + { + "left_line": 310, + "right_line": 316 + }, + { + "left_line": 311, + "right_line": 317 + }, + { + "left_line": 312, + "right_line": 318 + }, + { + "left_line": -1, + "right_line": 319 + }, + { + "left_line": 313, + "right_line": 320 + }, + { + "left_line": 314, + "right_line": 321 + }, + { + "left_line": 315, + "right_line": 322 + }, + { + "left_line": 316, + "right_line": 323 + }, + { + "left_line": 317, + "right_line": 324 + }, + { + "left_line": 318, + "right_line": 325 + }, + { + "left_line": 319, + "right_line": 326 + }, + { + "left_line": 320, + "right_line": 327 + }, + { + "left_line": 321, + "right_line": 328 + }, + { + "left_line": 322, + "right_line": 329 + }, + { + "left_line": 323, + "right_line": 330 + }, + { + "left_line": 324, + "right_line": 331 + }, + { + "left_line": 325, + "right_line": 332 + }, + { + "left_line": 326, + "right_line": 333 + }, + { + "left_line": 327, + "right_line": 334 + }, + { + "left_line": 328, + "right_line": 335 + }, + { + "left_line": 329, + "right_line": 336 + }, + { + "left_line": 330, + "right_line": 337 + }, + { + "left_line": 331, + "right_line": 338 + }, + { + "left_line": 332, + "right_line": 339 + }, + { + "left_line": 333, + "right_line": 340 + }, + { + "left_line": 334, + "right_line": 341 + }, + { + "left_line": 335, + "right_line": 342 + }, + { + "left_line": 336, + "right_line": 343 + }, + { + "left_line": 337, + "right_line": 344 + }, + { + "left_line": 338, + "right_line": 345 + }, + { + "left_line": 339, + "right_line": 346 + }, + { + "left_line": 340, + "right_line": 347 + }, + { + "left_line": 341, + "right_line": 348 + }, + { + "left_line": 342, + "right_line": 349 + }, + { + "left_line": 343, + "right_line": 350 + }, + { + "left_line": 344, + "right_line": 351 + }, + { + "left_line": 345, + "right_line": 352 + }, + { + "left_line": 346, + "right_line": 353 + }, + { + "left_line": 347, + "right_line": 354 + }, + { + "left_line": 348, + "right_line": 355 + }, + { + "left_line": 349, + "right_line": 356 + }, + { + "left_line": 350, + "right_line": 357 + }, + { + "left_line": 351, + "right_line": 358 + }, + { + "left_line": 352, + "right_line": 359 + }, + { + "left_line": -1, + "right_line": 360 + }, + { + "left_line": 353, + "right_line": 361 + }, + { + "left_line": 354, + "right_line": 362 + }, + { + "left_line": 355, + "right_line": 363 + }, + { + "left_line": 356, + "right_line": 364 + }, + { + "left_line": 357, + "right_line": 365 + }, + { + "left_line": 358, + "right_line": 366 + }, + { + "left_line": 359, + "right_line": 367 + }, + { + "left_line": 360, + "right_line": 368 + }, + { + "left_line": 361, + "right_line": 369 + }, + { + "left_line": 362, + "right_line": 370 + }, + { + "left_line": 363, + "right_line": 371 + }, + { + "left_line": 364, + "right_line": 372 + }, + { + "left_line": 365, + "right_line": 373 + }, + { + "left_line": 366, + "right_line": 374 + }, + { + "left_line": 367, + "right_line": 375 + }, + { + "left_line": 368, + "right_line": 376 + }, + { + "left_line": 369, + "right_line": 377 + }, + { + "left_line": 370, + "right_line": 378 + }, + { + "left_line": 371, + "right_line": 379 + }, + { + "left_line": 372, + "right_line": 380 + }, + { + "left_line": 373, + "right_line": 381 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_enable_rpc_sync_tests/metadata.json b/tests/testdata/lua_kong_enable_rpc_sync_tests/metadata.json new file mode 100644 index 0000000..c2db83a --- /dev/null +++ b/tests/testdata/lua_kong_enable_rpc_sync_tests/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "716a11b2aa995b7a67f3e6fbaf748669966bb448", + "commit": "d5fcb42", + "message": "tests(clustering/wasm): enable rpc sync tests for wasm partially (#14256)", + "author": "Chrono", + "date": "2025-02-17T09:11:10+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 16, + "deletions": 8 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_enable_rpc_sync_tests/new.lua b/tests/testdata/lua_kong_enable_rpc_sync_tests/new.lua new file mode 100644 index 0000000..a2b9dce --- /dev/null +++ b/tests/testdata/lua_kong_enable_rpc_sync_tests/new.lua @@ -0,0 +1,381 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson.safe" +local STATUS = require("kong.constants").CLUSTERING_SYNC_STATUS +local admin = require "spec.fixtures.admin_api" + + +local HEADER = "X-Proxy-Wasm" +local FILTER_SRC = "spec/fixtures/proxy_wasm_filters/build/response_transformer.wasm" + +local json = cjson.encode +local file = helpers.file +local random_string = require("kong.tools.rand").random_string +local uuid = require("kong.tools.uuid").uuid + + +local function expect_status(id, exp) + assert + .eventually(function() + local cp_client = helpers.admin_client() + + local res = cp_client:get("/clustering/data-planes/") + res:read_body() + + cp_client:close() + + local body = assert.response(res).has.jsonbody() + + if res.status ~= 200 then + return nil, { + msg = "bad http status", + exp = 200, + got = res.status, + } + end + + assert.is_table(body.data) + local found + for _, dp in ipairs(body.data) do + if dp.id == id then + found = dp + break + end + end + + if not found then + return nil, { + msg = "dp with id " .. id .. " not found in response", + res = body, + } + + elseif found.sync_status ~= exp then + return nil, { + msg = "unexpected sync_status", + exp = exp, + got = found.sync_status, + dp = found, + } + end + + return true + end) + .is_truthy("waiting for clustering sync status to equal " + .. "'filter_set_incompatible' for data plane") +end + +local function new_wasm_filter_directory() + local dir = helpers.make_temp_dir() + assert(file.copy(FILTER_SRC, dir .. "/response_transformer.wasm")) + + assert(file.copy(FILTER_SRC, dir .. "/response_transformer_with_schema.wasm")) + return dir +end + + +for _, v in ipairs({ {"off", "off"}, {"on", "off"}, {"on", "on"}, }) do + local rpc, rpc_sync = v[1], v[2] + +describe("#wasm - hybrid mode #postgres" .. " rpc_sync=" .. rpc_sync, function() + local cp_prefix = "cp" + local cp_errlog = cp_prefix .. "/logs/error.log" + local cp_filter_path + + local dp_prefix = "dp" + local dp_errlog = dp_prefix .. "/logs/error.log" + + lazy_setup(function() + helpers.clean_prefix(cp_prefix) + helpers.clean_prefix(dp_prefix) + + local _, db = helpers.get_db_utils("postgres", { + "services", + "routes", + "filter_chains", + "clustering_data_planes", + }) + + db.clustering_data_planes:truncate() + + cp_filter_path = new_wasm_filter_directory() + assert(file.write(cp_filter_path .. "/response_transformer_with_schema.meta.json", json { + config_schema = { + type = "object", + }, + })) + + assert(helpers.start_kong({ + role = "control_plane", + database = "postgres", + prefix = cp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + db_update_frequency = 0.1, + cluster_listen = "127.0.0.1:9005", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = true, + wasm_filters = "user", -- don't enable bundled filters for this test + wasm_filters_path = cp_filter_path, + nginx_main_worker_processes = 2, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + })) + + assert.logfile(cp_errlog).has.line([[successfully loaded "response_transformer" module]], true, 10) + assert.logfile(cp_errlog).has.no.line("[error]", true, 0) + assert.logfile(cp_errlog).has.no.line("[alert]", true, 0) + assert.logfile(cp_errlog).has.no.line("[crit]", true, 0) + assert.logfile(cp_errlog).has.no.line("[emerg]", true, 0) + end) + + lazy_teardown(function() + helpers.stop_kong(cp_prefix) + if cp_filter_path then + helpers.dir.rmtree(cp_filter_path) + end + end) + + describe("[happy path]", function() + local client + local dp_filter_path + local node_id + + lazy_setup(function() + dp_filter_path = new_wasm_filter_directory() + node_id = uuid() + + assert(helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = dp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + admin_listen = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = true, + wasm_filters = "user", -- don't enable bundled filters for this test + wasm_filters_path = dp_filter_path, + node_id = node_id, + nginx_main_worker_processes = 2, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + })) + + assert.logfile(dp_errlog).has.line([[successfully loaded "response_transformer" module]], true, 10) + assert.logfile(dp_errlog).has.no.line("[error]", true, 0) + assert.logfile(dp_errlog).has.no.line("[alert]", true, 0) + assert.logfile(dp_errlog).has.no.line("[crit]", true, 0) + assert.logfile(dp_errlog).has.no.line("[emerg]", true, 0) + + client = helpers.proxy_client() + end) + + lazy_teardown(function() + if client then client:close() end + helpers.stop_kong(dp_prefix) + if dp_filter_path then + helpers.dir.rmtree(dp_filter_path) + end + end) + + it("syncs wasm filter chains to the data plane", function() + local service = admin.services:insert({}) + local host = "wasm-" .. random_string() .. ".test" + + admin.routes:insert({ + service = service, + hosts = { host }, + }) + + local params = { + headers = { + host = host, + } + } + + assert + .eventually(function() + local res = client:get("/status/200", params) + return res.status == 200, { + exp = 200, + got = res.status, + res = res:read_body(), + } + end) + .is_truthy("service/route are ready on the data plane") + + local value = random_string() + + local filter = admin.filter_chains:insert({ + service = { id = service.id }, + filters = { + { + name = "response_transformer", + config = json { + append = { + headers = { + HEADER .. ":response_transformer", + }, + }, + } + }, + + { + name = "response_transformer_with_schema", + config = { + append = { + headers = { + HEADER .. ":response_transformer_with_schema", + }, + }, + } + }, + + } + }) + + assert + .eventually(function() + local res = client:get("/status/200", params) + res:read_body() + + if res.status ~= 200 then + return nil, { + msg = "bad http status", + exp = 200, + got = res.status, + res = res:read_body(), + } + end + + if res.headers[HEADER] + and type(res.headers[HEADER]) == "table" + and res.headers[HEADER][1] == "response_transformer" + and res.headers[HEADER][2] == "response_transformer_with_schema" + then + return true + end + + return nil, { + msg = "missing/incorrect " .. HEADER .. " header", + exp = value, + got = res.headers[HEADER] or "", + } + end) + .is_truthy("wasm filter is configured on the data plane") + + admin.filter_chains:remove({ id = filter.id }) + + assert + .eventually(function() + local res = client:get("/status/200", params) + res:read_body() + + if res.status ~= 200 then + return nil, { + msg = "bad http status", + exp = 200, + got = res.status, + res = res:read_body(), + } + end + + if res.headers[HEADER] ~= nil then + return nil, { + msg = "expected " .. HEADER .. " header to be absent", + exp = "", + got = res.headers[HEADER], + } + end + + return true + end) + .is_truthy("wasm filter has been removed from the data plane") + + expect_status(node_id, STATUS.NORMAL) + end) + end) + + -- XXX TODO: currently sync v2 does not support configuration compatibility check + local only_sync_v1 = rpc_sync == "on" and pending or describe + + only_sync_v1("data planes with wasm disabled", function() + local node_id + + lazy_setup(function() + helpers.clean_logfile(cp_errlog) + node_id = uuid() + + assert(helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = dp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + admin_listen = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = "off", + node_id = node_id, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + })) + end) + + + lazy_teardown(function() + helpers.stop_kong(dp_prefix) + end) + + it("does not sync configuration", function() + assert.logfile(cp_errlog).has.line( + [[unable to send updated configuration to data plane: data plane is missing one or more wasm filters]], + true, 5) + + expect_status(node_id, STATUS.FILTER_SET_INCOMPATIBLE) + end) + end) + + only_sync_v1("data planes missing one or more wasm filter", function() + local tmp_dir + local node_id + + lazy_setup(function() + helpers.clean_logfile(cp_errlog) + tmp_dir = helpers.make_temp_dir() + node_id = uuid() + + assert(helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = dp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + admin_listen = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = true, + wasm_filters = "user", -- don't enable bundled filters for this test + wasm_filters_path = tmp_dir, + node_id = node_id, + cluster_rpc = rpc, + cluster_rpc_sync = rpc_sync, + })) + end) + + + lazy_teardown(function() + helpers.stop_kong(dp_prefix) + helpers.dir.rmtree(tmp_dir) + end) + + it("does not sync configuration", function() + assert.logfile(cp_errlog).has.line( + [[unable to send updated configuration to data plane: data plane is missing one or more wasm filters]], + true, 5) + + expect_status(node_id, STATUS.FILTER_SET_INCOMPATIBLE) + end) + end) +end) +end -- for rpc_sync diff --git a/tests/testdata/lua_kong_enable_rpc_sync_tests/old.lua b/tests/testdata/lua_kong_enable_rpc_sync_tests/old.lua new file mode 100644 index 0000000..540331b --- /dev/null +++ b/tests/testdata/lua_kong_enable_rpc_sync_tests/old.lua @@ -0,0 +1,373 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson.safe" +local STATUS = require("kong.constants").CLUSTERING_SYNC_STATUS +local admin = require "spec.fixtures.admin_api" + + +local HEADER = "X-Proxy-Wasm" +local FILTER_SRC = "spec/fixtures/proxy_wasm_filters/build/response_transformer.wasm" + +local json = cjson.encode +local file = helpers.file +local random_string = require("kong.tools.rand").random_string +local uuid = require("kong.tools.uuid").uuid + + +local function expect_status(id, exp) + assert + .eventually(function() + local cp_client = helpers.admin_client() + + local res = cp_client:get("/clustering/data-planes/") + res:read_body() + + cp_client:close() + + local body = assert.response(res).has.jsonbody() + + if res.status ~= 200 then + return nil, { + msg = "bad http status", + exp = 200, + got = res.status, + } + end + + assert.is_table(body.data) + local found + for _, dp in ipairs(body.data) do + if dp.id == id then + found = dp + break + end + end + + if not found then + return nil, { + msg = "dp with id " .. id .. " not found in response", + res = body, + } + + elseif found.sync_status ~= exp then + return nil, { + msg = "unexpected sync_status", + exp = exp, + got = found.sync_status, + dp = found, + } + end + + return true + end) + .is_truthy("waiting for clustering sync status to equal " + .. "'filter_set_incompatible' for data plane") +end + +local function new_wasm_filter_directory() + local dir = helpers.make_temp_dir() + assert(file.copy(FILTER_SRC, dir .. "/response_transformer.wasm")) + + assert(file.copy(FILTER_SRC, dir .. "/response_transformer_with_schema.wasm")) + return dir +end + + +-- XXX TODO: enable rpc_sync = "on" +for _, rpc_sync in ipairs { "off" } do +describe("#wasm - hybrid mode #postgres" .. " rpc_sync=" .. rpc_sync, function() + local cp_prefix = "cp" + local cp_errlog = cp_prefix .. "/logs/error.log" + local cp_filter_path + + local dp_prefix = "dp" + local dp_errlog = dp_prefix .. "/logs/error.log" + + lazy_setup(function() + helpers.clean_prefix(cp_prefix) + helpers.clean_prefix(dp_prefix) + + local _, db = helpers.get_db_utils("postgres", { + "services", + "routes", + "filter_chains", + "clustering_data_planes", + }) + + db.clustering_data_planes:truncate() + + cp_filter_path = new_wasm_filter_directory() + assert(file.write(cp_filter_path .. "/response_transformer_with_schema.meta.json", json { + config_schema = { + type = "object", + }, + })) + + assert(helpers.start_kong({ + role = "control_plane", + database = "postgres", + prefix = cp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + db_update_frequency = 0.1, + cluster_listen = "127.0.0.1:9005", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = true, + wasm_filters = "user", -- don't enable bundled filters for this test + wasm_filters_path = cp_filter_path, + nginx_main_worker_processes = 2, + cluster_rpc_sync = rpc_sync, + })) + + assert.logfile(cp_errlog).has.line([[successfully loaded "response_transformer" module]], true, 10) + assert.logfile(cp_errlog).has.no.line("[error]", true, 0) + assert.logfile(cp_errlog).has.no.line("[alert]", true, 0) + assert.logfile(cp_errlog).has.no.line("[crit]", true, 0) + assert.logfile(cp_errlog).has.no.line("[emerg]", true, 0) + end) + + lazy_teardown(function() + helpers.stop_kong(cp_prefix) + if cp_filter_path then + helpers.dir.rmtree(cp_filter_path) + end + end) + + describe("[happy path]", function() + local client + local dp_filter_path + local node_id + + lazy_setup(function() + dp_filter_path = new_wasm_filter_directory() + node_id = uuid() + + assert(helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = dp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + admin_listen = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = true, + wasm_filters = "user", -- don't enable bundled filters for this test + wasm_filters_path = dp_filter_path, + node_id = node_id, + nginx_main_worker_processes = 2, + cluster_rpc_sync = rpc_sync, + })) + + assert.logfile(dp_errlog).has.line([[successfully loaded "response_transformer" module]], true, 10) + assert.logfile(dp_errlog).has.no.line("[error]", true, 0) + assert.logfile(dp_errlog).has.no.line("[alert]", true, 0) + assert.logfile(dp_errlog).has.no.line("[crit]", true, 0) + assert.logfile(dp_errlog).has.no.line("[emerg]", true, 0) + + client = helpers.proxy_client() + end) + + lazy_teardown(function() + if client then client:close() end + helpers.stop_kong(dp_prefix) + if dp_filter_path then + helpers.dir.rmtree(dp_filter_path) + end + end) + + it("syncs wasm filter chains to the data plane", function() + local service = admin.services:insert({}) + local host = "wasm-" .. random_string() .. ".test" + + admin.routes:insert({ + service = service, + hosts = { host }, + }) + + local params = { + headers = { + host = host, + } + } + + assert + .eventually(function() + local res = client:get("/status/200", params) + return res.status == 200, { + exp = 200, + got = res.status, + res = res:read_body(), + } + end) + .is_truthy("service/route are ready on the data plane") + + local value = random_string() + + local filter = admin.filter_chains:insert({ + service = { id = service.id }, + filters = { + { + name = "response_transformer", + config = json { + append = { + headers = { + HEADER .. ":response_transformer", + }, + }, + } + }, + + { + name = "response_transformer_with_schema", + config = { + append = { + headers = { + HEADER .. ":response_transformer_with_schema", + }, + }, + } + }, + + } + }) + + assert + .eventually(function() + local res = client:get("/status/200", params) + res:read_body() + + if res.status ~= 200 then + return nil, { + msg = "bad http status", + exp = 200, + got = res.status, + res = res:read_body(), + } + end + + if res.headers[HEADER] + and type(res.headers[HEADER]) == "table" + and res.headers[HEADER][1] == "response_transformer" + and res.headers[HEADER][2] == "response_transformer_with_schema" + then + return true + end + + return nil, { + msg = "missing/incorrect " .. HEADER .. " header", + exp = value, + got = res.headers[HEADER] or "", + } + end) + .is_truthy("wasm filter is configured on the data plane") + + admin.filter_chains:remove({ id = filter.id }) + + assert + .eventually(function() + local res = client:get("/status/200", params) + res:read_body() + + if res.status ~= 200 then + return nil, { + msg = "bad http status", + exp = 200, + got = res.status, + res = res:read_body(), + } + end + + if res.headers[HEADER] ~= nil then + return nil, { + msg = "expected " .. HEADER .. " header to be absent", + exp = "", + got = res.headers[HEADER], + } + end + + return true + end) + .is_truthy("wasm filter has been removed from the data plane") + + expect_status(node_id, STATUS.NORMAL) + end) + end) + + describe("data planes with wasm disabled", function() + local node_id + + lazy_setup(function() + helpers.clean_logfile(cp_errlog) + node_id = uuid() + + assert(helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = dp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + admin_listen = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = "off", + node_id = node_id, + cluster_rpc_sync = rpc_sync, + })) + end) + + + lazy_teardown(function() + helpers.stop_kong(dp_prefix) + end) + + it("does not sync configuration", function() + assert.logfile(cp_errlog).has.line( + [[unable to send updated configuration to data plane: data plane is missing one or more wasm filters]], + true, 5) + + expect_status(node_id, STATUS.FILTER_SET_INCOMPATIBLE) + end) + end) + + describe("data planes missing one or more wasm filter", function() + local tmp_dir + local node_id + + lazy_setup(function() + helpers.clean_logfile(cp_errlog) + tmp_dir = helpers.make_temp_dir() + node_id = uuid() + + assert(helpers.start_kong({ + role = "data_plane", + database = "off", + prefix = dp_prefix, + cluster_cert = "spec/fixtures/kong_clustering.crt", + cluster_cert_key = "spec/fixtures/kong_clustering.key", + cluster_control_plane = "127.0.0.1:9005", + admin_listen = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + wasm = true, + wasm_filters = "user", -- don't enable bundled filters for this test + wasm_filters_path = tmp_dir, + node_id = node_id, + cluster_rpc_sync = rpc_sync, + })) + end) + + + lazy_teardown(function() + helpers.stop_kong(dp_prefix) + helpers.dir.rmtree(tmp_dir) + end) + + it("does not sync configuration", function() + assert.logfile(cp_errlog).has.line( + [[unable to send updated configuration to data plane: data plane is missing one or more wasm filters]], + true, 5) + + expect_status(node_id, STATUS.FILTER_SET_INCOMPATIBLE) + end) + end) +end) +end -- for rpc_sync diff --git a/tests/testdata/lua_kong_fix_default_workspace_check/expected.json b/tests/testdata/lua_kong_fix_default_workspace_check/expected.json new file mode 100644 index 0000000..74ef9d6 --- /dev/null +++ b/tests/testdata/lua_kong_fix_default_workspace_check/expected.json @@ -0,0 +1,2889 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 1 + ], + "type": "comment", + "start_byte": 7950, + "end_byte": 8010 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3 + ], + "type": "block", + "start_byte": 7912, + "end_byte": 8310 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 7912, + "end_byte": 7917 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0 + ], + "type": "variable_declaration", + "start_byte": 7912, + "end_byte": 7945 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 7918, + "end_byte": 7930 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1 + ], + "type": "assignment_statement", + "start_byte": 7918, + "end_byte": 7945 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 7931, + "end_byte": 7932 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1 + ], + "type": "assignment_statement", + "start_byte": 7918, + "end_byte": 7945 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8150 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8202 + }, + "position": 0 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 2, + 2, + 0 + ], + "type": "dot_index_expression", + "start_byte": 8013, + "end_byte": 8025 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 2 + ], + "type": "expression_list", + "start_byte": 7933, + "end_byte": 7945 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 2, + 2 + ], + "type": "dot_index_expression", + "start_byte": 8013, + "end_byte": 8028 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 7933, + "dest_end_byte": 7945 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8109 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8150 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 8110, + "end_byte": 8113 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8150 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 2 + ], + "type": "binary_expression", + "start_byte": 8120, + "end_byte": 8150 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8150 + }, + "position": 2, + "subtree": true + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 7915, + "end_byte": 7941 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8074 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 7915, + "end_byte": 7976 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 8018, + "dest_end_byte": 8044 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 7942, + "end_byte": 7945 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8074 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 7915, + "end_byte": 7976 + }, + "old_position": 1, + "dest_start_byte": 8045, + "dest_end_byte": 8048 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 8075, + "end_byte": 8078 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8109 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 2 + ], + "type": "binary_expression", + "start_byte": 8085, + "end_byte": 8109 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0 + ], + "type": "binary_expression", + "start_byte": 8018, + "end_byte": 8109 + }, + "position": 2, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 2, + 2, + 0 + ], + "type": "identifier", + "label": "delta_entity", + "start_byte": 8187, + "end_byte": 8199 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 2, + 2 + ], + "type": "dot_index_expression", + "start_byte": 8187, + "end_byte": 8202 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 3, + 0, + 2, + 0, + 0 + ], + "type": "identifier", + "label": "delta_entity", + "start_byte": 8243, + "end_byte": 8255 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 3, + 0, + 2, + 0 + ], + "type": "dot_index_expression", + "start_byte": 8243, + "end_byte": 8258 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0, + 2, + 0 + ], + "type": "identifier", + "label": "delta_entity", + "start_byte": 8055, + "end_byte": 8067 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0, + 2 + ], + "type": "binary_expression", + "start_byte": 8055, + "end_byte": 8074 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0, + 2, + 1 + ], + "type": "comparison_operator_literal", + "label": "~=", + "start_byte": 8068, + "end_byte": 8070 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0, + 2 + ], + "type": "binary_expression", + "start_byte": 8055, + "end_byte": 8074 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0, + 2, + 2 + ], + "type": "nil", + "label": "nil", + "start_byte": 8071, + "end_byte": 8074 + }, + "parent": { + "tree": "after", + "path": [ + 40, + 4, + 15, + 3, + 2, + 1, + 0, + 0, + 0, + 2 + ], + "type": "binary_expression", + "start_byte": 8055, + "end_byte": 8074 + }, + "position": 2 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0, + 2, + 0 + ], + "type": "dot_index_expression", + "start_byte": 7946, + "end_byte": 7963 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0, + 2, + 1 + ], + "type": "comparison_operator_literal", + "label": "==", + "start_byte": 7964, + "end_byte": 7966 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 1, + 0, + 2, + 2 + ], + "type": "string", + "label": "\"default\"", + "start_byte": 7967, + "end_byte": 7976 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 40, + 4, + 15, + 3, + 0, + 3, + 0, + 2, + 0, + 0 + ], + "type": "dot_index_expression", + "start_byte": 8069, + "end_byte": 8081 + }, + "subtree": true + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": -1, + "right_line": 286 + }, + { + "left_line": -1, + "right_line": 287 + }, + { + "left_line": 286, + "right_line": 288 + }, + { + "left_line": 287, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 289 + }, + { + "left_line": -1, + "right_line": 290 + }, + { + "left_line": -1, + "right_line": 291 + }, + { + "left_line": -1, + "right_line": 292 + }, + { + "left_line": 288, + "right_line": 293 + }, + { + "left_line": 289, + "right_line": 294 + }, + { + "left_line": 290, + "right_line": 295 + }, + { + "left_line": 291, + "right_line": 296 + }, + { + "left_line": 292, + "right_line": 297 + }, + { + "left_line": 293, + "right_line": 298 + }, + { + "left_line": 294, + "right_line": 299 + }, + { + "left_line": 295, + "right_line": 300 + }, + { + "left_line": 296, + "right_line": 301 + }, + { + "left_line": 297, + "right_line": 302 + }, + { + "left_line": 298, + "right_line": 303 + }, + { + "left_line": 299, + "right_line": 304 + }, + { + "left_line": 300, + "right_line": 305 + }, + { + "left_line": 301, + "right_line": 306 + }, + { + "left_line": 302, + "right_line": 307 + }, + { + "left_line": 303, + "right_line": 308 + }, + { + "left_line": 304, + "right_line": 309 + }, + { + "left_line": 305, + "right_line": 310 + }, + { + "left_line": 306, + "right_line": 311 + }, + { + "left_line": 307, + "right_line": 312 + }, + { + "left_line": 308, + "right_line": 313 + }, + { + "left_line": 309, + "right_line": 314 + }, + { + "left_line": 310, + "right_line": 315 + }, + { + "left_line": 311, + "right_line": 316 + }, + { + "left_line": 312, + "right_line": 317 + }, + { + "left_line": 313, + "right_line": 318 + }, + { + "left_line": 314, + "right_line": 319 + }, + { + "left_line": 315, + "right_line": 320 + }, + { + "left_line": 316, + "right_line": 321 + }, + { + "left_line": 317, + "right_line": 322 + }, + { + "left_line": 318, + "right_line": 323 + }, + { + "left_line": 319, + "right_line": 324 + }, + { + "left_line": 320, + "right_line": 325 + }, + { + "left_line": 321, + "right_line": 326 + }, + { + "left_line": 322, + "right_line": 327 + }, + { + "left_line": 323, + "right_line": 328 + }, + { + "left_line": 324, + "right_line": 329 + }, + { + "left_line": 325, + "right_line": 330 + }, + { + "left_line": 326, + "right_line": 331 + }, + { + "left_line": 327, + "right_line": 332 + }, + { + "left_line": 328, + "right_line": 333 + }, + { + "left_line": 329, + "right_line": 334 + }, + { + "left_line": 330, + "right_line": 335 + }, + { + "left_line": 331, + "right_line": 336 + }, + { + "left_line": 332, + "right_line": 337 + }, + { + "left_line": 333, + "right_line": 338 + }, + { + "left_line": 334, + "right_line": 339 + }, + { + "left_line": 335, + "right_line": 340 + }, + { + "left_line": 336, + "right_line": 341 + }, + { + "left_line": 337, + "right_line": 342 + }, + { + "left_line": 338, + "right_line": 343 + }, + { + "left_line": 339, + "right_line": 344 + }, + { + "left_line": 340, + "right_line": 345 + }, + { + "left_line": 341, + "right_line": 346 + }, + { + "left_line": 342, + "right_line": 347 + }, + { + "left_line": 343, + "right_line": 348 + }, + { + "left_line": 344, + "right_line": 349 + }, + { + "left_line": 345, + "right_line": 350 + }, + { + "left_line": 346, + "right_line": 351 + }, + { + "left_line": 347, + "right_line": 352 + }, + { + "left_line": 348, + "right_line": 353 + }, + { + "left_line": 349, + "right_line": 354 + }, + { + "left_line": 350, + "right_line": 355 + }, + { + "left_line": 351, + "right_line": 356 + }, + { + "left_line": 352, + "right_line": 357 + }, + { + "left_line": 353, + "right_line": 358 + }, + { + "left_line": 354, + "right_line": 359 + }, + { + "left_line": 355, + "right_line": 360 + }, + { + "left_line": 356, + "right_line": 361 + }, + { + "left_line": 357, + "right_line": 362 + }, + { + "left_line": 358, + "right_line": 363 + }, + { + "left_line": 359, + "right_line": 364 + }, + { + "left_line": 360, + "right_line": 365 + }, + { + "left_line": 361, + "right_line": 366 + }, + { + "left_line": 362, + "right_line": 367 + }, + { + "left_line": 363, + "right_line": 368 + }, + { + "left_line": 364, + "right_line": 369 + }, + { + "left_line": 365, + "right_line": 370 + }, + { + "left_line": 366, + "right_line": 371 + }, + { + "left_line": 367, + "right_line": 372 + }, + { + "left_line": 368, + "right_line": 373 + }, + { + "left_line": 369, + "right_line": 374 + }, + { + "left_line": 370, + "right_line": 375 + }, + { + "left_line": 371, + "right_line": 376 + }, + { + "left_line": 372, + "right_line": 377 + }, + { + "left_line": 373, + "right_line": 378 + }, + { + "left_line": 374, + "right_line": 379 + }, + { + "left_line": 375, + "right_line": 380 + }, + { + "left_line": 376, + "right_line": 381 + }, + { + "left_line": 377, + "right_line": 382 + }, + { + "left_line": 378, + "right_line": 383 + }, + { + "left_line": 379, + "right_line": 384 + }, + { + "left_line": 380, + "right_line": 385 + }, + { + "left_line": 381, + "right_line": 386 + }, + { + "left_line": 382, + "right_line": 387 + }, + { + "left_line": 383, + "right_line": 388 + }, + { + "left_line": 384, + "right_line": 389 + }, + { + "left_line": 385, + "right_line": 390 + }, + { + "left_line": 386, + "right_line": 391 + }, + { + "left_line": 387, + "right_line": 392 + }, + { + "left_line": 388, + "right_line": 393 + }, + { + "left_line": 389, + "right_line": 394 + }, + { + "left_line": 390, + "right_line": 395 + }, + { + "left_line": 391, + "right_line": 396 + }, + { + "left_line": 392, + "right_line": 397 + }, + { + "left_line": 393, + "right_line": 398 + }, + { + "left_line": 394, + "right_line": 399 + }, + { + "left_line": 395, + "right_line": 400 + }, + { + "left_line": 396, + "right_line": 401 + }, + { + "left_line": 397, + "right_line": 402 + }, + { + "left_line": 398, + "right_line": 403 + }, + { + "left_line": 399, + "right_line": 404 + }, + { + "left_line": 400, + "right_line": 405 + }, + { + "left_line": 401, + "right_line": 406 + }, + { + "left_line": 402, + "right_line": 407 + }, + { + "left_line": 403, + "right_line": 408 + }, + { + "left_line": 404, + "right_line": 409 + }, + { + "left_line": 405, + "right_line": 410 + }, + { + "left_line": 406, + "right_line": 411 + }, + { + "left_line": 407, + "right_line": 412 + }, + { + "left_line": 408, + "right_line": 413 + }, + { + "left_line": 409, + "right_line": 414 + }, + { + "left_line": 410, + "right_line": 415 + }, + { + "left_line": 411, + "right_line": 416 + }, + { + "left_line": 412, + "right_line": 417 + }, + { + "left_line": 413, + "right_line": 418 + }, + { + "left_line": 414, + "right_line": 419 + }, + { + "left_line": 415, + "right_line": 420 + }, + { + "left_line": 416, + "right_line": 421 + }, + { + "left_line": 417, + "right_line": 422 + }, + { + "left_line": 418, + "right_line": 423 + }, + { + "left_line": 419, + "right_line": 424 + }, + { + "left_line": 420, + "right_line": 425 + }, + { + "left_line": 421, + "right_line": 426 + }, + { + "left_line": 422, + "right_line": 427 + }, + { + "left_line": 423, + "right_line": 428 + }, + { + "left_line": 424, + "right_line": 429 + }, + { + "left_line": 425, + "right_line": 430 + }, + { + "left_line": 426, + "right_line": 431 + }, + { + "left_line": 427, + "right_line": 432 + }, + { + "left_line": 428, + "right_line": 433 + }, + { + "left_line": 429, + "right_line": 434 + }, + { + "left_line": 430, + "right_line": 435 + }, + { + "left_line": 431, + "right_line": 436 + }, + { + "left_line": 432, + "right_line": 437 + }, + { + "left_line": 433, + "right_line": 438 + }, + { + "left_line": 434, + "right_line": 439 + }, + { + "left_line": 435, + "right_line": 440 + }, + { + "left_line": 436, + "right_line": 441 + }, + { + "left_line": 437, + "right_line": 442 + }, + { + "left_line": 438, + "right_line": 443 + }, + { + "left_line": 439, + "right_line": 444 + }, + { + "left_line": 440, + "right_line": 445 + }, + { + "left_line": 441, + "right_line": 446 + }, + { + "left_line": 442, + "right_line": 447 + }, + { + "left_line": 443, + "right_line": 448 + }, + { + "left_line": 444, + "right_line": 449 + }, + { + "left_line": 445, + "right_line": 450 + }, + { + "left_line": 446, + "right_line": 451 + }, + { + "left_line": 447, + "right_line": 452 + }, + { + "left_line": 448, + "right_line": 453 + }, + { + "left_line": 449, + "right_line": 454 + }, + { + "left_line": 450, + "right_line": 455 + }, + { + "left_line": 451, + "right_line": 456 + }, + { + "left_line": 452, + "right_line": 457 + }, + { + "left_line": 453, + "right_line": 458 + }, + { + "left_line": 454, + "right_line": 459 + }, + { + "left_line": 455, + "right_line": 460 + }, + { + "left_line": 456, + "right_line": 461 + }, + { + "left_line": 457, + "right_line": 462 + }, + { + "left_line": 458, + "right_line": 463 + }, + { + "left_line": 459, + "right_line": 464 + }, + { + "left_line": 460, + "right_line": 465 + }, + { + "left_line": 461, + "right_line": 466 + }, + { + "left_line": 462, + "right_line": 467 + }, + { + "left_line": 463, + "right_line": 468 + }, + { + "left_line": 464, + "right_line": 469 + }, + { + "left_line": 465, + "right_line": 470 + }, + { + "left_line": 466, + "right_line": 471 + }, + { + "left_line": 467, + "right_line": 472 + }, + { + "left_line": 468, + "right_line": 473 + }, + { + "left_line": 469, + "right_line": 474 + }, + { + "left_line": 470, + "right_line": 475 + }, + { + "left_line": 471, + "right_line": 476 + }, + { + "left_line": 472, + "right_line": 477 + }, + { + "left_line": 473, + "right_line": 478 + }, + { + "left_line": 474, + "right_line": 479 + }, + { + "left_line": 475, + "right_line": 480 + }, + { + "left_line": 476, + "right_line": 481 + }, + { + "left_line": 477, + "right_line": 482 + }, + { + "left_line": 478, + "right_line": 483 + }, + { + "left_line": 479, + "right_line": 484 + }, + { + "left_line": 480, + "right_line": 485 + }, + { + "left_line": 481, + "right_line": 486 + }, + { + "left_line": 482, + "right_line": 487 + }, + { + "left_line": 483, + "right_line": 488 + }, + { + "left_line": 484, + "right_line": 489 + }, + { + "left_line": 485, + "right_line": 490 + }, + { + "left_line": 486, + "right_line": 491 + }, + { + "left_line": 487, + "right_line": 492 + }, + { + "left_line": 488, + "right_line": 493 + }, + { + "left_line": 489, + "right_line": 494 + }, + { + "left_line": 490, + "right_line": 495 + }, + { + "left_line": 491, + "right_line": 496 + }, + { + "left_line": 492, + "right_line": 497 + }, + { + "left_line": 493, + "right_line": 498 + }, + { + "left_line": 494, + "right_line": 499 + }, + { + "left_line": 495, + "right_line": 500 + }, + { + "left_line": 496, + "right_line": 501 + }, + { + "left_line": 497, + "right_line": 502 + }, + { + "left_line": 498, + "right_line": 503 + }, + { + "left_line": 499, + "right_line": 504 + }, + { + "left_line": 500, + "right_line": 505 + }, + { + "left_line": 501, + "right_line": 506 + }, + { + "left_line": 502, + "right_line": 507 + }, + { + "left_line": 503, + "right_line": 508 + }, + { + "left_line": 504, + "right_line": 509 + }, + { + "left_line": 505, + "right_line": 510 + }, + { + "left_line": 506, + "right_line": 511 + }, + { + "left_line": 507, + "right_line": 512 + }, + { + "left_line": 508, + "right_line": 513 + }, + { + "left_line": 509, + "right_line": 514 + }, + { + "left_line": 510, + "right_line": 515 + }, + { + "left_line": 511, + "right_line": 516 + }, + { + "left_line": 512, + "right_line": 517 + }, + { + "left_line": 513, + "right_line": 518 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_fix_default_workspace_check/metadata.json b/tests/testdata/lua_kong_fix_default_workspace_check/metadata.json new file mode 100644 index 0000000..0fd089e --- /dev/null +++ b/tests/testdata/lua_kong_fix_default_workspace_check/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "8eaeef1200deafa263dcc2f1b5d2e4fd1e09e033", + "commit": "3d921b1", + "message": "fix(clustering/sync): fix default workspace check (#14220)", + "author": "Haoxuan", + "date": "2025-01-31T16:26:17+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 8, + "deletions": 3 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_fix_default_workspace_check/new.lua b/tests/testdata/lua_kong_fix_default_workspace_check/new.lua new file mode 100644 index 0000000..3dbacf6 --- /dev/null +++ b/tests/testdata/lua_kong_fix_default_workspace_check/new.lua @@ -0,0 +1,518 @@ +local _M = {} +local _MT = { __index = _M, } + + +local txn = require("resty.lmdb.transaction") +local declarative = require("kong.db.declarative") +local constants = require("kong.constants") +local concurrency = require("kong.concurrency") +local isempty = require("table.isempty") +local events = require("kong.runloop.events") +local EMPTY = require("kong.tools.table").EMPTY + + +local validate_deltas = require("kong.clustering.services.sync.validate").validate_deltas +local insert_entity_for_txn = declarative.insert_entity_for_txn +local delete_entity_for_txn = declarative.delete_entity_for_txn +local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY +local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY +local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH +local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY +local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS +local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, } +local MAX_RETRY = 5 + + +local assert = assert +local ipairs = ipairs +local ngx_null = ngx.null +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_WARN = ngx.WARN +local ngx_INFO = ngx.INFO +local ngx_DEBUG = ngx.DEBUG + + +function _M.new(strategy) + local self = { + strategy = strategy, + } + + return setmetatable(self, _MT) +end + + +local function empty_sync_result() + return { default = { deltas = {}, wipe = false, }, } +end + + +local function full_sync_result() + local deltas, err = declarative.export_config_sync() + if not deltas then + return nil, err + end + + -- wipe dp lmdb, full sync + return { default = { deltas = deltas, wipe = true, }, } +end + + +local function get_current_version() + return declarative.get_current_hash() or DECLARATIVE_EMPTY_CONFIG_HASH +end + + +function _M:init_cp(manager) + local purge_delay = manager.conf.cluster_data_plane_purge_delay + + -- CP + -- Method: kong.sync.v2.get_delta + -- Params: versions: list of current versions of the database + -- example: { default = { version = "1000", }, } + manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions) + ngx_log(ngx_DEBUG, "[kong.sync.v2] config push (connected client)") + + local rpc_peers + if kong.rpc then + rpc_peers = kong.rpc:get_peers() + end + + local default_namespace = current_versions.default + + if not default_namespace then + return nil, "default namespace does not exist inside params" + end + + -- { default = { version = "1000", }, } + local default_namespace_version = default_namespace.version + local node_info = assert(kong.rpc:get_peer_info(node_id)) + + -- follow update_sync_status() in control_plane.lua + local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, { + last_seen = ngx.time(), + hostname = node_id, + ip = node_info.ip, -- get the correct ip + version = node_info.version, -- get from rpc call + labels = node_info.labels, -- get from rpc call + cert_details = node_info.cert_details, -- get from rpc call + sync_status = CLUSTERING_SYNC_STATUS.NORMAL, + config_hash = default_namespace_version, + rpc_capabilities = rpc_peers and rpc_peers[node_id] or EMPTY, + }, { ttl = purge_delay, no_broadcast_crud_event = true, }) + if not ok then + ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err) + end + + local latest_version, err = self.strategy:get_latest_version() + if not latest_version then + return nil, err + end + + -- string comparison effectively does the same as number comparison + if not self.strategy:is_valid_version(default_namespace_version) or + default_namespace_version ~= latest_version then + return full_sync_result() + end + + return empty_sync_result() + end) +end + + +function _M:init_dp(manager) + local kong_shm = ngx.shared.kong + -- DP + -- Method: kong.sync.v2.notify_new_version + -- Params: new_versions: list of namespaces and their new versions, like: + -- { default = { new_version = "1000", }, } + manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions) + -- TODO: currently only default is supported, and anything else is ignored + local default_new_version = new_versions.default + if not default_new_version then + return nil, "default namespace does not exist inside params" + end + + local version = default_new_version.new_version + if not version then + return nil, "'new_version' key does not exist" + end + + local lmdb_ver = get_current_version() + if lmdb_ver < version then + -- set lastest version to shm + kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version) + return self:sync_once() + end + + ngx_log(ngx_DEBUG, "no sync runs, version is ", version) + + return true + end) +end + + +function _M:init(manager, is_cp) + if is_cp then + self:init_cp(manager) + else + self:init_dp(manager) + end +end + + +-- check if rpc connection is ready +local function is_rpc_ready() + -- TODO: find a better way to detect when the RPC layer, including caps list, + -- has been fully initialized, instead of waiting for up to 5.5 seconds + for i = 1, 10 do + local res = kong.rpc:get_peers() + + -- control_plane is ready + if res["control_plane"] then + return true + end + + -- retry later + ngx.sleep(0.1 * i) + end +end + + +-- tell cp we already updated the version by rpc notification +local function update_status(ver) + local msg = { default = { version = ver, }, } + + local ok, err = kong.rpc:notify("control_plane", "kong.sync.v2.get_delta", msg) + if not ok then + ngx_log(ngx_ERR, "update status notification failed: ", err) + end +end + + +local function lmdb_update(db, t, delta, opts, is_full_sync) + local delta_type = delta.type + local delta_entity = delta.entity + + -- upsert the entity + -- delete if exists + local old_entity, err = db[delta_type]:select(delta_entity) + if err then + return nil, err + end + + if old_entity and not is_full_sync then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + local res, err = insert_entity_for_txn(t, delta_type, delta_entity, opts) + if not res then + return nil, err + end + + if is_full_sync then + return nil + end + + return { delta_type, old_entity and "update" or "create", delta_entity, old_entity, } +end + + +local function lmdb_delete(db, t, delta, opts, is_full_sync) + local delta_type = delta.type + + local old_entity, err = db[delta_type]:select(delta.pk, opts) + if err then + return nil, err + end + + -- full sync requires extra torlerance for missing entities + if not old_entity then + return nil + end + + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + + if is_full_sync then + return nil + end + + return { delta_type, "delete", old_entity, } +end + + +local function do_sync() + if not is_rpc_ready() then + return nil, "rpc is not ready" + end + + local msg = { default = { version = get_current_version(), }, } + + local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg) + if not ns_deltas then + ngx_log(ngx_ERR, "sync get_delta error: ", err) + return true + end + + -- ns_deltas should look like: + -- { default = { deltas = { ... }, wipe = true, }, } + + local ns_delta = ns_deltas.default + if not ns_delta then + return nil, "default namespace does not exist inside params" + end + + local wipe = ns_delta.wipe + local deltas = ns_delta.deltas + + if not deltas then + return nil, "sync get_delta error: deltas is null" + end + + if isempty(deltas) then + -- no delta to sync + return true + end + + -- we should find the correct default workspace + -- and replace the old one with it + local default_ws_changed + for _, delta in ipairs(deltas) do + local delta_entity = delta.entity + -- Update default workspace if delta is for workspace update + if delta.type == "workspaces" and + delta_entity ~= nil and + delta_entity ~= ngx_null and + delta_entity.name == "default" and + kong.default_workspace ~= delta_entity.id + then + kong.default_workspace = delta_entity.id + default_ws_changed = true + break + end + end + assert(type(kong.default_workspace) == "string") + + -- validate deltas + local ok, err = validate_deltas(deltas, wipe) + if not ok then + return nil, err + end + + local t = txn.begin(512) + + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync begins") + + t:db_drop(false) + end + + local db = kong.db + + local version = "" + local opts = {} + local crud_events = {} + local crud_events_n = 0 + + -- delta should look like: + -- { type = ..., entity = { ... }, version = "1", ws_id = ..., } + for _, delta in ipairs(deltas) do + local delta_version = delta.version + local delta_type = delta.type + local delta_entity = delta.entity + + -- delta should have ws_id to generate the correct lmdb key + -- if entity is workspaceable + -- set the correct workspace for item + opts.workspace = delta.ws_id + + local is_update = delta_entity ~= nil and delta_entity ~= ngx_null + local operation_name = is_update and "update" or "delete" + local operation = is_update and lmdb_update or lmdb_delete + + -- log the operation before executing it, so when failing we know what entity caused it + ngx_log(ngx_DEBUG, + "[kong.sync.v2] ", operation_name, " entity", + ", version: ", delta_version, + ", type: ", delta_type) + + local ev, err = operation(db, t, delta, opts, wipe) + if err then + return nil, err + end + + if ev then + crud_events_n = crud_events_n + 1 + crud_events[crud_events_n] = ev + end + + -- delta.version should not be nil or ngx.null + assert(type(delta_version) == "string") + + if delta_version ~= version then + version = delta_version + end + end -- for _, delta + + -- store current sync version + t:set(DECLARATIVE_HASH_KEY, version) + + -- record the default workspace into LMDB for any of the following case: + -- * wipe is false, but the default workspace has been changed + -- * wipe is true (full sync) + if default_ws_changed or wipe then + t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace) + end + + local ok, err = t:commit() + if not ok then + return nil, err + end + + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync ends") + + kong.core_cache:purge() + kong.cache:purge() + + -- Trigger other workers' callbacks like reconfigure_handler. + -- + -- Full sync could rebuild route, plugins and balancer route, so their + -- hashes are nil. + local reconfigure_data = { kong.default_workspace, nil, nil, nil, } + local ok, err = events.declarative_reconfigure_notify(reconfigure_data) + if not ok then + return nil, err + end + + else + for _, event in ipairs(crud_events) do + -- delta_type, crud_event_type, delta.entity, old_entity + db[event[1]]:post_crud_event(event[2], event[3], event[4]) + end + end + + return true +end + + +local function sync_handler(premature) + if premature then + return + end + + local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync) + if not res and err ~= "timeout" then + ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err) + end + + return res, err +end + + +local function sync_once_impl(premature, retry_count) + if premature then + return + end + + local version_before_sync = get_current_version() + + local _, err = sync_handler() + + -- check if "kong.sync.v2.notify_new_version" updates the latest version + + local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY) + if not latest_notified_version then + ngx_log(ngx_DEBUG, "no version notified yet") + return + end + + local current_version = get_current_version() + if current_version >= latest_notified_version then + ngx_log(ngx_DEBUG, "version already updated") + + -- version changed, we should update status + if version_before_sync ~= current_version then + update_status(current_version) + end + + return + end + + -- retry if the version is not updated + retry_count = retry_count or 0 + + if retry_count >= MAX_RETRY then + ngx_log(ngx_WARN, "sync_once retry count exceeded. retry_count: ", retry_count) + return + end + + -- we do not count a timed out sync. just retry + if err ~= "timeout" then + retry_count = retry_count + 1 + end + + -- in some cases, the new spawned timer will be switched to immediately, + -- preventing the coroutine who possesses the mutex to run + -- to let other coroutines has a chance to run + local ok, err = kong.timer:at(0.1, sync_once_impl, retry_count) + -- this is a workaround for a timerng bug, where tail recursion causes failure + -- ok could be a string so let's convert it to boolean + if not ok then + return nil, err + end + return true +end + + +function _M:sync_once(delay) + local name = "rpc_sync_v2_once" + local is_managed = kong.timer:is_managed(name) + + -- we are running a sync handler + if is_managed then + return true + end + + local ok, err = kong.timer:named_at(name, delay or 0, sync_once_impl, 0) + if not ok then + return nil, err + end + + return true +end + + +function _M:sync_every(delay, stop) + local name = "rpc_sync_v2_every" + local is_managed = kong.timer:is_managed(name) + + -- we only start or stop once + + if stop then + if is_managed then + assert(kong.timer:cancel(name)) + end + return true + end + + if is_managed then + return true + end + + local ok, err = kong.timer:named_every(name, delay, sync_handler) + if not ok then + return nil, err + end + + return true +end + + +return _M diff --git a/tests/testdata/lua_kong_fix_default_workspace_check/old.lua b/tests/testdata/lua_kong_fix_default_workspace_check/old.lua new file mode 100644 index 0000000..47329a3 --- /dev/null +++ b/tests/testdata/lua_kong_fix_default_workspace_check/old.lua @@ -0,0 +1,513 @@ +local _M = {} +local _MT = { __index = _M, } + + +local txn = require("resty.lmdb.transaction") +local declarative = require("kong.db.declarative") +local constants = require("kong.constants") +local concurrency = require("kong.concurrency") +local isempty = require("table.isempty") +local events = require("kong.runloop.events") +local EMPTY = require("kong.tools.table").EMPTY + + +local validate_deltas = require("kong.clustering.services.sync.validate").validate_deltas +local insert_entity_for_txn = declarative.insert_entity_for_txn +local delete_entity_for_txn = declarative.delete_entity_for_txn +local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY +local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY +local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH +local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY +local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS +local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, } +local MAX_RETRY = 5 + + +local assert = assert +local ipairs = ipairs +local ngx_null = ngx.null +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_WARN = ngx.WARN +local ngx_INFO = ngx.INFO +local ngx_DEBUG = ngx.DEBUG + + +function _M.new(strategy) + local self = { + strategy = strategy, + } + + return setmetatable(self, _MT) +end + + +local function empty_sync_result() + return { default = { deltas = {}, wipe = false, }, } +end + + +local function full_sync_result() + local deltas, err = declarative.export_config_sync() + if not deltas then + return nil, err + end + + -- wipe dp lmdb, full sync + return { default = { deltas = deltas, wipe = true, }, } +end + + +local function get_current_version() + return declarative.get_current_hash() or DECLARATIVE_EMPTY_CONFIG_HASH +end + + +function _M:init_cp(manager) + local purge_delay = manager.conf.cluster_data_plane_purge_delay + + -- CP + -- Method: kong.sync.v2.get_delta + -- Params: versions: list of current versions of the database + -- example: { default = { version = "1000", }, } + manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions) + ngx_log(ngx_DEBUG, "[kong.sync.v2] config push (connected client)") + + local rpc_peers + if kong.rpc then + rpc_peers = kong.rpc:get_peers() + end + + local default_namespace = current_versions.default + + if not default_namespace then + return nil, "default namespace does not exist inside params" + end + + -- { default = { version = "1000", }, } + local default_namespace_version = default_namespace.version + local node_info = assert(kong.rpc:get_peer_info(node_id)) + + -- follow update_sync_status() in control_plane.lua + local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, { + last_seen = ngx.time(), + hostname = node_id, + ip = node_info.ip, -- get the correct ip + version = node_info.version, -- get from rpc call + labels = node_info.labels, -- get from rpc call + cert_details = node_info.cert_details, -- get from rpc call + sync_status = CLUSTERING_SYNC_STATUS.NORMAL, + config_hash = default_namespace_version, + rpc_capabilities = rpc_peers and rpc_peers[node_id] or EMPTY, + }, { ttl = purge_delay, no_broadcast_crud_event = true, }) + if not ok then + ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err) + end + + local latest_version, err = self.strategy:get_latest_version() + if not latest_version then + return nil, err + end + + -- string comparison effectively does the same as number comparison + if not self.strategy:is_valid_version(default_namespace_version) or + default_namespace_version ~= latest_version then + return full_sync_result() + end + + return empty_sync_result() + end) +end + + +function _M:init_dp(manager) + local kong_shm = ngx.shared.kong + -- DP + -- Method: kong.sync.v2.notify_new_version + -- Params: new_versions: list of namespaces and their new versions, like: + -- { default = { new_version = "1000", }, } + manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions) + -- TODO: currently only default is supported, and anything else is ignored + local default_new_version = new_versions.default + if not default_new_version then + return nil, "default namespace does not exist inside params" + end + + local version = default_new_version.new_version + if not version then + return nil, "'new_version' key does not exist" + end + + local lmdb_ver = get_current_version() + if lmdb_ver < version then + -- set lastest version to shm + kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version) + return self:sync_once() + end + + ngx_log(ngx_DEBUG, "no sync runs, version is ", version) + + return true + end) +end + + +function _M:init(manager, is_cp) + if is_cp then + self:init_cp(manager) + else + self:init_dp(manager) + end +end + + +-- check if rpc connection is ready +local function is_rpc_ready() + -- TODO: find a better way to detect when the RPC layer, including caps list, + -- has been fully initialized, instead of waiting for up to 5.5 seconds + for i = 1, 10 do + local res = kong.rpc:get_peers() + + -- control_plane is ready + if res["control_plane"] then + return true + end + + -- retry later + ngx.sleep(0.1 * i) + end +end + + +-- tell cp we already updated the version by rpc notification +local function update_status(ver) + local msg = { default = { version = ver, }, } + + local ok, err = kong.rpc:notify("control_plane", "kong.sync.v2.get_delta", msg) + if not ok then + ngx_log(ngx_ERR, "update status notification failed: ", err) + end +end + + +local function lmdb_update(db, t, delta, opts, is_full_sync) + local delta_type = delta.type + local delta_entity = delta.entity + + -- upsert the entity + -- delete if exists + local old_entity, err = db[delta_type]:select(delta_entity) + if err then + return nil, err + end + + if old_entity and not is_full_sync then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + local res, err = insert_entity_for_txn(t, delta_type, delta_entity, opts) + if not res then + return nil, err + end + + if is_full_sync then + return nil + end + + return { delta_type, old_entity and "update" or "create", delta_entity, old_entity, } +end + + +local function lmdb_delete(db, t, delta, opts, is_full_sync) + local delta_type = delta.type + + local old_entity, err = db[delta_type]:select(delta.pk, opts) + if err then + return nil, err + end + + -- full sync requires extra torlerance for missing entities + if not old_entity then + return nil + end + + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + + if is_full_sync then + return nil + end + + return { delta_type, "delete", old_entity, } +end + + +local function do_sync() + if not is_rpc_ready() then + return nil, "rpc is not ready" + end + + local msg = { default = { version = get_current_version(), }, } + + local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg) + if not ns_deltas then + ngx_log(ngx_ERR, "sync get_delta error: ", err) + return true + end + + -- ns_deltas should look like: + -- { default = { deltas = { ... }, wipe = true, }, } + + local ns_delta = ns_deltas.default + if not ns_delta then + return nil, "default namespace does not exist inside params" + end + + local wipe = ns_delta.wipe + local deltas = ns_delta.deltas + + if not deltas then + return nil, "sync get_delta error: deltas is null" + end + + if isempty(deltas) then + -- no delta to sync + return true + end + + -- we should find the correct default workspace + -- and replace the old one with it + local default_ws_changed + for _, delta in ipairs(deltas) do + if delta.type == "workspaces" and delta.entity.name == "default" and + kong.default_workspace ~= delta.entity.id + then + kong.default_workspace = delta.entity.id + default_ws_changed = true + break + end + end + assert(type(kong.default_workspace) == "string") + + -- validate deltas + local ok, err = validate_deltas(deltas, wipe) + if not ok then + return nil, err + end + + local t = txn.begin(512) + + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync begins") + + t:db_drop(false) + end + + local db = kong.db + + local version = "" + local opts = {} + local crud_events = {} + local crud_events_n = 0 + + -- delta should look like: + -- { type = ..., entity = { ... }, version = "1", ws_id = ..., } + for _, delta in ipairs(deltas) do + local delta_version = delta.version + local delta_type = delta.type + local delta_entity = delta.entity + + -- delta should have ws_id to generate the correct lmdb key + -- if entity is workspaceable + -- set the correct workspace for item + opts.workspace = delta.ws_id + + local is_update = delta_entity ~= nil and delta_entity ~= ngx_null + local operation_name = is_update and "update" or "delete" + local operation = is_update and lmdb_update or lmdb_delete + + -- log the operation before executing it, so when failing we know what entity caused it + ngx_log(ngx_DEBUG, + "[kong.sync.v2] ", operation_name, " entity", + ", version: ", delta_version, + ", type: ", delta_type) + + local ev, err = operation(db, t, delta, opts, wipe) + if err then + return nil, err + end + + if ev then + crud_events_n = crud_events_n + 1 + crud_events[crud_events_n] = ev + end + + -- delta.version should not be nil or ngx.null + assert(type(delta_version) == "string") + + if delta_version ~= version then + version = delta_version + end + end -- for _, delta + + -- store current sync version + t:set(DECLARATIVE_HASH_KEY, version) + + -- record the default workspace into LMDB for any of the following case: + -- * wipe is false, but the default workspace has been changed + -- * wipe is true (full sync) + if default_ws_changed or wipe then + t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace) + end + + local ok, err = t:commit() + if not ok then + return nil, err + end + + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync ends") + + kong.core_cache:purge() + kong.cache:purge() + + -- Trigger other workers' callbacks like reconfigure_handler. + -- + -- Full sync could rebuild route, plugins and balancer route, so their + -- hashes are nil. + local reconfigure_data = { kong.default_workspace, nil, nil, nil, } + local ok, err = events.declarative_reconfigure_notify(reconfigure_data) + if not ok then + return nil, err + end + + else + for _, event in ipairs(crud_events) do + -- delta_type, crud_event_type, delta.entity, old_entity + db[event[1]]:post_crud_event(event[2], event[3], event[4]) + end + end + + return true +end + + +local function sync_handler(premature) + if premature then + return + end + + local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync) + if not res and err ~= "timeout" then + ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err) + end + + return res, err +end + + +local function sync_once_impl(premature, retry_count) + if premature then + return + end + + local version_before_sync = get_current_version() + + local _, err = sync_handler() + + -- check if "kong.sync.v2.notify_new_version" updates the latest version + + local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY) + if not latest_notified_version then + ngx_log(ngx_DEBUG, "no version notified yet") + return + end + + local current_version = get_current_version() + if current_version >= latest_notified_version then + ngx_log(ngx_DEBUG, "version already updated") + + -- version changed, we should update status + if version_before_sync ~= current_version then + update_status(current_version) + end + + return + end + + -- retry if the version is not updated + retry_count = retry_count or 0 + + if retry_count >= MAX_RETRY then + ngx_log(ngx_WARN, "sync_once retry count exceeded. retry_count: ", retry_count) + return + end + + -- we do not count a timed out sync. just retry + if err ~= "timeout" then + retry_count = retry_count + 1 + end + + -- in some cases, the new spawned timer will be switched to immediately, + -- preventing the coroutine who possesses the mutex to run + -- to let other coroutines has a chance to run + local ok, err = kong.timer:at(0.1, sync_once_impl, retry_count) + -- this is a workaround for a timerng bug, where tail recursion causes failure + -- ok could be a string so let's convert it to boolean + if not ok then + return nil, err + end + return true +end + + +function _M:sync_once(delay) + local name = "rpc_sync_v2_once" + local is_managed = kong.timer:is_managed(name) + + -- we are running a sync handler + if is_managed then + return true + end + + local ok, err = kong.timer:named_at(name, delay or 0, sync_once_impl, 0) + if not ok then + return nil, err + end + + return true +end + + +function _M:sync_every(delay, stop) + local name = "rpc_sync_v2_every" + local is_managed = kong.timer:is_managed(name) + + -- we only start or stop once + + if stop then + if is_managed then + assert(kong.timer:cancel(name)) + end + return true + end + + if is_managed then + return true + end + + local ok, err = kong.timer:named_every(name, delay, sync_handler) + if not ok then + return nil, err + end + + return true +end + + +return _M diff --git a/tests/testdata/lua_kong_sync_retry_timeout/expected.json b/tests/testdata/lua_kong_sync_retry_timeout/expected.json new file mode 100644 index 0000000..73ea5ab --- /dev/null +++ b/tests/testdata/lua_kong_sync_retry_timeout/expected.json @@ -0,0 +1,3341 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 10776, + "end_byte": 10781 + }, + "parent": { + "tree": "after", + "path": [ + 36 + ], + "type": "function_declaration", + "start_byte": 10776, + "end_byte": 12131 + }, + "position": 0 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 37, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 10956 + }, + "parent": { + "tree": "after", + "path": [ + 36 + ], + "type": "function_declaration", + "start_byte": 10776, + "end_byte": 12131 + }, + "position": 4, + "old_parent": { + "tree": "before", + "path": [ + 37 + ], + "type": "function_declaration", + "start_byte": 10780, + "end_byte": 10960 + }, + "old_position": 4, + "dest_start_byte": 10832, + "dest_end_byte": 12127 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 37, + 4, + 0 + ], + "type": "variable_declaration", + "start_byte": 10832, + "end_byte": 10898 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 15, + "old_parent": { + "tree": "before", + "path": [ + 37, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 10956 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 11864, + "dest_end_byte": 11932 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 37, + 4, + 1 + ], + "type": "if_statement", + "start_byte": 10901, + "end_byte": 10941 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 18, + "old_parent": { + "tree": "before", + "path": [ + 37, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 10956 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 12073, + "dest_end_byte": 12113, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 37, + 4, + 2 + ], + "type": "return_statement", + "start_byte": 10945, + "end_byte": 10956 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 19, + "old_parent": { + "tree": "before", + "path": [ + 37, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 10956 + }, + "old_position": 2, + "subtree": true, + "dest_start_byte": 12116, + "dest_end_byte": 12127, + "group_id": "group-1" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 35, + 4, + 3 + ], + "type": "return_statement", + "start_byte": 10754, + "end_byte": 10769 + }, + "parent": { + "tree": "after", + "path": [ + 35, + 4 + ], + "type": "block", + "start_byte": 10525, + "end_byte": 10769 + }, + "position": 3, + "subtree": true + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 0 + ], + "type": "if_statement", + "start_byte": 11013, + "end_byte": 11047 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 10832, + "dest_end_byte": 10866 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 2 + ], + "type": "comment", + "start_byte": 10903, + "end_byte": 10975 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 2 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 3 + ], + "type": "variable_declaration", + "start_byte": 11145, + "end_byte": 11239 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 3, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 3, + "subtree": true, + "dest_start_byte": 10979, + "dest_end_byte": 11073, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 4 + ], + "type": "if_statement", + "start_byte": 11242, + "end_byte": 11344 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 4, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 4, + "subtree": true, + "dest_start_byte": 11076, + "dest_end_byte": 11178, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 5 + ], + "type": "variable_declaration", + "start_byte": 11348, + "end_byte": 11393 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 5, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 5, + "subtree": true, + "dest_start_byte": 11182, + "dest_end_byte": 11227, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 6 + ], + "type": "if_statement", + "start_byte": 11396, + "end_byte": 11513 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 6, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 6, + "subtree": true, + "dest_start_byte": 11230, + "dest_end_byte": 11347, + "group_id": "group-2" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 7 + ], + "type": "comment", + "start_byte": 11351, + "end_byte": 11389 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 7 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 8 + ], + "type": "assignment_statement", + "start_byte": 11558, + "end_byte": 11588 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 8, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 8, + "subtree": true, + "dest_start_byte": 11392, + "dest_end_byte": 11422, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 9 + ], + "type": "if_statement", + "start_byte": 11591, + "end_byte": 11722 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 9, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 9, + "subtree": true, + "dest_start_byte": 11426, + "dest_end_byte": 11557, + "group_id": "group-2" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 10 + ], + "type": "comment", + "start_byte": 11561, + "end_byte": 11608 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 10 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11 + ], + "type": "if_statement", + "start_byte": 11611, + "end_byte": 11640 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 11 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 12 + ], + "type": "comment", + "start_byte": 11679, + "end_byte": 11751 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 12 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 13 + ], + "type": "comment", + "start_byte": 11754, + "end_byte": 11812 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 13 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 14 + ], + "type": "comment", + "start_byte": 11815, + "end_byte": 11861 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 14 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 16 + ], + "type": "comment", + "start_byte": 11935, + "end_byte": 12013 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 16 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 17 + ], + "type": "comment", + "start_byte": 12016, + "end_byte": 12070 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4 + ], + "type": "block", + "start_byte": 10832, + "end_byte": 12127 + }, + "position": 17 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 1, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 10870, + "end_byte": 10875 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 1 + ], + "type": "variable_declaration", + "start_byte": 10870, + "end_byte": 10899 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 11611, + "end_byte": 11613 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11 + ], + "type": "if_statement", + "start_byte": 11611, + "end_byte": 11675 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 1 + ], + "type": "binary_expression", + "start_byte": 11614, + "end_byte": 11630 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11 + ], + "type": "if_statement", + "start_byte": 11611, + "end_byte": 11675 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 11631, + "end_byte": 11635 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11 + ], + "type": "if_statement", + "start_byte": 11611, + "end_byte": 11675 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 4 + ], + "type": "end", + "label": "end", + "start_byte": 11672, + "end_byte": 11675 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11 + ], + "type": "if_statement", + "start_byte": 11611, + "end_byte": 11675 + }, + "position": 4 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 1, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 10876, + "end_byte": 10882 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 1, + 1 + ], + "type": "assignment_statement", + "start_byte": 10876, + "end_byte": 10899 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 1, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 10883, + "end_byte": 10884 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 1, + 1 + ], + "type": "assignment_statement", + "start_byte": 10876, + "end_byte": 10899 + }, + "position": 1 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 1 + ], + "type": "function_call", + "start_byte": 11051, + "end_byte": 11065 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 1, + 1, + 2 + ], + "type": "expression_list", + "start_byte": 10885, + "end_byte": 10899 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 10885, + "dest_end_byte": 10899 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 3, + 0, + 0 + ], + "type": "variable_list", + "start_byte": 11640, + "end_byte": 11651 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 3, + 0 + ], + "type": "assignment_statement", + "start_byte": 11640, + "end_byte": 11669 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 3, + 0, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 11652, + "end_byte": 11653 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 3, + 0 + ], + "type": "assignment_statement", + "start_byte": 11640, + "end_byte": 11669 + }, + "position": 1 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 10, + 1, + 0, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 11755, + "end_byte": 11770 + }, + "parent": { + "tree": "after", + "path": [ + 36, + 4, + 11, + 3, + 0, + 2 + ], + "type": "expression_list", + "start_byte": 11654, + "end_byte": 11669 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 38, + 3, + 10, + 1, + 0, + 1 + ], + "type": "arguments", + "start_byte": 11754, + "end_byte": 11771 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 11654, + "dest_end_byte": 11669 + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 37, + 4, + 0, + 1, + 2, + 0, + 1, + 0 + ], + "type": "number", + "label": "0", + "start_byte": 10862, + "end_byte": 10863 + }, + "old_value": "0", + "new_value": "0.1", + "dest_node": { + "tree": "after", + "path": [ + 36, + 4, + 15, + 1, + 2, + 0, + 1, + 0 + ], + "type": "number", + "label": "0.1", + "start_byte": 11894, + "end_byte": 11897 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 36 + ], + "type": "variable_declaration", + "start_byte": 10757, + "end_byte": 10777 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 37, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 10780, + "end_byte": 10785 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 37, + 1 + ], + "type": "function", + "label": "function", + "start_byte": 10786, + "end_byte": 10794 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 37, + 2 + ], + "type": "identifier", + "label": "start_sync_once_timer", + "start_byte": 10795, + "end_byte": 10816 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 37, + 3 + ], + "type": "parameters", + "start_byte": 10816, + "end_byte": 10829 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 37, + 5 + ], + "type": "end", + "label": "end", + "start_byte": 10957, + "end_byte": 10960 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 37 + ], + "type": "function_declaration", + "start_byte": 10780, + "end_byte": 10832 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 2 + ], + "type": "comment", + "start_byte": 11069, + "end_byte": 11141 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 7 + ], + "type": "comment", + "start_byte": 11517, + "end_byte": 11555 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 10, + 0 + ], + "type": "return", + "label": "return", + "start_byte": 11726, + "end_byte": 11732 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 10, + 1, + 0, + 0 + ], + "type": "identifier", + "label": "start_sync_once_timer", + "start_byte": 11733, + "end_byte": 11754 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 10, + 1, + 0, + 1 + ], + "type": "arguments", + "start_byte": 11754, + "end_byte": 11771 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3, + 10, + 1 + ], + "type": "expression_list", + "start_byte": 11733, + "end_byte": 11771 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 38, + 3 + ], + "type": "block", + "start_byte": 11013, + "end_byte": 11771 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": -1, + "right_line": 376 + }, + { + "left_line": -1, + "right_line": 377 + }, + { + "left_line": 376, + "right_line": 378 + }, + { + "left_line": 377, + "right_line": 379 + }, + { + "left_line": 378, + "right_line": 380 + }, + { + "left_line": 379, + "right_line": 381 + }, + { + "left_line": -1, + "right_line": 382 + }, + { + "left_line": -1, + "right_line": 383 + }, + { + "left_line": -1, + "right_line": 384 + }, + { + "left_line": 380, + "right_line": 385 + }, + { + "left_line": -1, + "right_line": 386 + }, + { + "left_line": 381, + "right_line": 387 + }, + { + "left_line": 382, + "right_line": 388 + }, + { + "left_line": -1, + "right_line": 389 + }, + { + "left_line": -1, + "right_line": 390 + }, + { + "left_line": -1, + "right_line": 391 + }, + { + "left_line": -1, + "right_line": 392 + }, + { + "left_line": -1, + "right_line": 393 + }, + { + "left_line": -1, + "right_line": 394 + }, + { + "left_line": -1, + "right_line": 395 + }, + { + "left_line": -1, + "right_line": 396 + }, + { + "left_line": -1, + "right_line": 397 + }, + { + "left_line": -1, + "right_line": 398 + }, + { + "left_line": -1, + "right_line": 399 + }, + { + "left_line": -1, + "right_line": 400 + }, + { + "left_line": -1, + "right_line": 401 + }, + { + "left_line": -1, + "right_line": 402 + }, + { + "left_line": -1, + "right_line": 403 + }, + { + "left_line": -1, + "right_line": 404 + }, + { + "left_line": -1, + "right_line": 405 + }, + { + "left_line": -1, + "right_line": 406 + }, + { + "left_line": -1, + "right_line": 407 + }, + { + "left_line": -1, + "right_line": 408 + }, + { + "left_line": -1, + "right_line": 409 + }, + { + "left_line": -1, + "right_line": 410 + }, + { + "left_line": -1, + "right_line": 411 + }, + { + "left_line": -1, + "right_line": 412 + }, + { + "left_line": -1, + "right_line": 413 + }, + { + "left_line": -1, + "right_line": 414 + }, + { + "left_line": -1, + "right_line": 415 + }, + { + "left_line": -1, + "right_line": 416 + }, + { + "left_line": -1, + "right_line": 417 + }, + { + "left_line": 383, + "right_line": 418 + }, + { + "left_line": -1, + "right_line": 419 + }, + { + "left_line": -1, + "right_line": 420 + }, + { + "left_line": 384, + "right_line": 421 + }, + { + "left_line": 385, + "right_line": 422 + }, + { + "left_line": 386, + "right_line": 423 + }, + { + "left_line": 387, + "right_line": -1 + }, + { + "left_line": 388, + "right_line": 424 + }, + { + "left_line": 389, + "right_line": -1 + }, + { + "left_line": 390, + "right_line": -1 + }, + { + "left_line": 391, + "right_line": -1 + }, + { + "left_line": 392, + "right_line": -1 + }, + { + "left_line": 393, + "right_line": -1 + }, + { + "left_line": 394, + "right_line": -1 + }, + { + "left_line": 395, + "right_line": -1 + }, + { + "left_line": 396, + "right_line": -1 + }, + { + "left_line": 397, + "right_line": -1 + }, + { + "left_line": 398, + "right_line": -1 + }, + { + "left_line": 399, + "right_line": -1 + }, + { + "left_line": 400, + "right_line": -1 + }, + { + "left_line": 401, + "right_line": -1 + }, + { + "left_line": 402, + "right_line": -1 + }, + { + "left_line": 403, + "right_line": -1 + }, + { + "left_line": 404, + "right_line": -1 + }, + { + "left_line": 405, + "right_line": -1 + }, + { + "left_line": 406, + "right_line": -1 + }, + { + "left_line": 407, + "right_line": -1 + }, + { + "left_line": 408, + "right_line": -1 + }, + { + "left_line": 409, + "right_line": -1 + }, + { + "left_line": 410, + "right_line": -1 + }, + { + "left_line": 411, + "right_line": -1 + }, + { + "left_line": 412, + "right_line": -1 + }, + { + "left_line": 413, + "right_line": -1 + }, + { + "left_line": 414, + "right_line": -1 + }, + { + "left_line": 415, + "right_line": -1 + }, + { + "left_line": 416, + "right_line": -1 + }, + { + "left_line": 417, + "right_line": -1 + }, + { + "left_line": 418, + "right_line": -1 + }, + { + "left_line": 419, + "right_line": -1 + }, + { + "left_line": 420, + "right_line": -1 + }, + { + "left_line": 421, + "right_line": 425 + }, + { + "left_line": 422, + "right_line": 426 + }, + { + "left_line": 423, + "right_line": 427 + }, + { + "left_line": 424, + "right_line": 428 + }, + { + "left_line": 425, + "right_line": 429 + }, + { + "left_line": 426, + "right_line": 430 + }, + { + "left_line": 427, + "right_line": 431 + }, + { + "left_line": 428, + "right_line": 432 + }, + { + "left_line": 429, + "right_line": 433 + }, + { + "left_line": 430, + "right_line": 434 + }, + { + "left_line": 431, + "right_line": 435 + }, + { + "left_line": 432, + "right_line": 436 + }, + { + "left_line": 433, + "right_line": 437 + }, + { + "left_line": 434, + "right_line": 438 + }, + { + "left_line": 435, + "right_line": 439 + }, + { + "left_line": 436, + "right_line": 440 + }, + { + "left_line": 437, + "right_line": 441 + }, + { + "left_line": 438, + "right_line": 442 + }, + { + "left_line": 439, + "right_line": 443 + }, + { + "left_line": 440, + "right_line": 444 + }, + { + "left_line": 441, + "right_line": 445 + }, + { + "left_line": 442, + "right_line": 446 + }, + { + "left_line": 443, + "right_line": 447 + }, + { + "left_line": 444, + "right_line": 448 + }, + { + "left_line": 445, + "right_line": 449 + }, + { + "left_line": 446, + "right_line": 450 + }, + { + "left_line": 447, + "right_line": 451 + }, + { + "left_line": 448, + "right_line": 452 + }, + { + "left_line": 449, + "right_line": 453 + }, + { + "left_line": 450, + "right_line": 454 + }, + { + "left_line": 451, + "right_line": 455 + }, + { + "left_line": 452, + "right_line": 456 + }, + { + "left_line": 453, + "right_line": 457 + }, + { + "left_line": 454, + "right_line": 458 + }, + { + "left_line": 455, + "right_line": 459 + }, + { + "left_line": 456, + "right_line": 460 + }, + { + "left_line": 457, + "right_line": 461 + }, + { + "left_line": 458, + "right_line": 462 + }, + { + "left_line": 459, + "right_line": 463 + }, + { + "left_line": 460, + "right_line": 464 + }, + { + "left_line": 461, + "right_line": 465 + }, + { + "left_line": 462, + "right_line": 466 + }, + { + "left_line": 463, + "right_line": 467 + }, + { + "left_line": 464, + "right_line": 468 + }, + { + "left_line": 465, + "right_line": 469 + }, + { + "left_line": 466, + "right_line": 470 + }, + { + "left_line": 467, + "right_line": 471 + }, + { + "left_line": 468, + "right_line": 472 + }, + { + "left_line": 469, + "right_line": 473 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_sync_retry_timeout/metadata.json b/tests/testdata/lua_kong_sync_retry_timeout/metadata.json new file mode 100644 index 0000000..33ee6a1 --- /dev/null +++ b/tests/testdata/lua_kong_sync_retry_timeout/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/Kong/kong", + "parent": "b72823235fd668b9d716a1fb18e58dcb52f6f73c", + "commit": "dfc955e", + "message": "fix(clustering/rpc): sync retry timeout due to block (#14195)", + "author": "Xumin", + "date": "2025-01-21T15:47:11+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 19, + "deletions": 15 +} \ No newline at end of file diff --git a/tests/testdata/lua_kong_sync_retry_timeout/new.lua b/tests/testdata/lua_kong_sync_retry_timeout/new.lua new file mode 100644 index 0000000..00fea1e --- /dev/null +++ b/tests/testdata/lua_kong_sync_retry_timeout/new.lua @@ -0,0 +1,473 @@ +local _M = {} +local _MT = { __index = _M, } + + +local txn = require("resty.lmdb.transaction") +local declarative = require("kong.db.declarative") +local constants = require("kong.constants") +local concurrency = require("kong.concurrency") +local isempty = require("table.isempty") +local events = require("kong.runloop.events") +local EMPTY = require("kong.tools.table").EMPTY + + +local insert_entity_for_txn = declarative.insert_entity_for_txn +local delete_entity_for_txn = declarative.delete_entity_for_txn +local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY +local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY +local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH +local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY +local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS +local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, } +local MAX_RETRY = 5 + + +local assert = assert +local ipairs = ipairs +local ngx_null = ngx.null +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_INFO = ngx.INFO +local ngx_DEBUG = ngx.DEBUG + + +function _M.new(strategy) + local self = { + strategy = strategy, + } + + return setmetatable(self, _MT) +end + + +local function empty_sync_result() + return { default = { deltas = {}, wipe = false, }, } +end + + +local function full_sync_result() + local deltas, err = declarative.export_config_sync() + if not deltas then + return nil, err + end + + -- wipe dp lmdb, full sync + return { default = { deltas = deltas, wipe = true, }, } +end + + +local function get_current_version() + return declarative.get_current_hash() or DECLARATIVE_EMPTY_CONFIG_HASH +end + + +function _M:init_cp(manager) + local purge_delay = manager.conf.cluster_data_plane_purge_delay + + -- CP + -- Method: kong.sync.v2.get_delta + -- Params: versions: list of current versions of the database + -- example: { default = { version = "1000", }, } + manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions) + ngx_log(ngx_DEBUG, "[kong.sync.v2] config push (connected client)") + + local rpc_peers + if kong.rpc then + rpc_peers = kong.rpc:get_peers() + end + + local default_namespace = current_versions.default + + if not default_namespace then + return nil, "default namespace does not exist inside params" + end + + -- { default = { version = "1000", }, } + local default_namespace_version = default_namespace.version + local node_info = assert(kong.rpc:get_peer_info(node_id)) + + -- follow update_sync_status() in control_plane.lua + local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, { + last_seen = ngx.time(), + hostname = node_id, + ip = node_info.ip, -- get the correct ip + version = node_info.version, -- get from rpc call + labels = node_info.labels, -- get from rpc call + cert_details = node_info.cert_details, -- get from rpc call + sync_status = CLUSTERING_SYNC_STATUS.NORMAL, + config_hash = default_namespace_version, + rpc_capabilities = rpc_peers and rpc_peers[node_id] or EMPTY, + }, { ttl = purge_delay, no_broadcast_crud_event = true, }) + if not ok then + ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err) + end + + local latest_version, err = self.strategy:get_latest_version() + if not latest_version then + return nil, err + end + + -- string comparison effectively does the same as number comparison + if not self.strategy:is_valid_version(default_namespace_version) or + default_namespace_version ~= latest_version then + return full_sync_result() + end + + return empty_sync_result() + end) +end + + +function _M:init_dp(manager) + local kong_shm = ngx.shared.kong + -- DP + -- Method: kong.sync.v2.notify_new_version + -- Params: new_versions: list of namespaces and their new versions, like: + -- { default = { new_version = "1000", }, } + manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions) + -- TODO: currently only default is supported, and anything else is ignored + local default_new_version = new_versions.default + if not default_new_version then + return nil, "default namespace does not exist inside params" + end + + local version = default_new_version.new_version + if not version then + return nil, "'new_version' key does not exist" + end + + local lmdb_ver = get_current_version() + if lmdb_ver < version then + -- set lastest version to shm + kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version) + return self:sync_once() + end + + ngx_log(ngx_DEBUG, "no sync runs, version is ", version) + + return true + end) +end + + +function _M:init(manager, is_cp) + if is_cp then + self:init_cp(manager) + else + self:init_dp(manager) + end +end + + +-- check if rpc connection is ready +local function is_rpc_ready() + -- TODO: find a better way to detect when the RPC layer, including caps list, + -- has been fully initialized, instead of waiting for up to 5.5 seconds + for i = 1, 10 do + local res = kong.rpc:get_peers() + + -- control_plane is ready + if res["control_plane"] then + return true + end + + -- retry later + ngx.sleep(0.1 * i) + end +end + + +local function do_sync() + if not is_rpc_ready() then + return nil, "rpc is not ready" + end + + local msg = { default = { version = get_current_version(), }, } + + local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg) + if not ns_deltas then + ngx_log(ngx_ERR, "sync get_delta error: ", err) + return true + end + + -- ns_deltas should look like: + -- { default = { deltas = { ... }, wipe = true, }, } + + local ns_delta = ns_deltas.default + if not ns_delta then + return nil, "default namespace does not exist inside params" + end + + local deltas = ns_delta.deltas + + if not deltas then + return nil, "sync get_delta error: deltas is null" + end + + if isempty(deltas) then + -- no delta to sync + return true + end + + -- we should find the correct default workspace + -- and replace the old one with it + local default_ws_changed + for _, delta in ipairs(deltas) do + if delta.type == "workspaces" and delta.entity.name == "default" and + kong.default_workspace ~= delta.entity.id + then + kong.default_workspace = delta.entity.id + default_ws_changed = true + break + end + end + assert(type(kong.default_workspace) == "string") + + local t = txn.begin(512) + + local wipe = ns_delta.wipe + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync begins") + + t:db_drop(false) + end + + local db = kong.db + + local version = "" + local opts = {} + local crud_events = {} + local crud_events_n = 0 + + -- delta should look like: + -- { type = ..., entity = { ... }, version = "1", ws_id = ..., } + for _, delta in ipairs(deltas) do + local delta_version = delta.version + local delta_type = delta.type + local delta_entity = delta.entity + local ev + + -- delta should have ws_id to generate the correct lmdb key + -- if entity is workspaceable + -- set the correct workspace for item + opts.workspace = delta.ws_id + + if delta_entity ~= nil and delta_entity ~= ngx_null then + -- upsert the entity + -- does the entity already exists? + local old_entity, err = db[delta_type]:select(delta_entity) + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + local res, err = insert_entity_for_txn(t, delta_type, delta_entity, opts) + if not res then + return nil, err + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] update entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, old_entity and "update" or "create", delta_entity, old_entity, } + end + + else + -- delete the entity, opts for getting correct lmdb key + local old_entity, err = db[delta_type]:select(delta.pk, opts) -- composite key + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] delete entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, "delete", old_entity, } + end + end -- if delta_entity ~= nil and delta_entity ~= ngx_null + + -- wipe the whole lmdb, should not have events + if not wipe then + crud_events_n = crud_events_n + 1 + crud_events[crud_events_n] = ev + end + + -- delta.version should not be nil or ngx.null + assert(type(delta_version) == "string") + + if delta_version ~= version then + version = delta_version + end + end -- for _, delta + + -- store current sync version + t:set(DECLARATIVE_HASH_KEY, version) + + -- record the default workspace into LMDB for any of the following case: + -- * wipe is false, but the default workspace has been changed + -- * wipe is true (full sync) + if default_ws_changed or wipe then + t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace) + end + + local ok, err = t:commit() + if not ok then + return nil, err + end + + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync ends") + + kong.core_cache:purge() + kong.cache:purge() + + -- Trigger other workers' callbacks like reconfigure_handler. + -- + -- Full sync could rebuild route, plugins and balancer route, so their + -- hashes are nil. + local reconfigure_data = { kong.default_workspace, nil, nil, nil, } + local ok, err = events.declarative_reconfigure_notify(reconfigure_data) + if not ok then + return nil, err + end + + else + for _, event in ipairs(crud_events) do + -- delta_type, crud_event_type, delta.entity, old_entity + db[event[1]]:post_crud_event(event[2], event[3], event[4]) + end + end + + return true +end + + +local function sync_handler(premature) + if premature then + return + end + + local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync) + if not res and err ~= "timeout" then + ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err) + end + + return res, err +end + + +local function sync_once_impl(premature, retry_count) + if premature then + return + end + + local _, err = sync_handler() + + -- check if "kong.sync.v2.notify_new_version" updates the latest version + + local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY) + if not latest_notified_version then + ngx_log(ngx_DEBUG, "no version notified yet") + return + end + + local current_version = get_current_version() + if current_version >= latest_notified_version then + ngx_log(ngx_DEBUG, "version already updated") + return + end + + -- retry if the version is not updated + retry_count = retry_count or 0 + + if retry_count > MAX_RETRY then + ngx_log(ngx_ERR, "sync_once retry count exceeded. retry_count: ", retry_count) + return + end + + -- we do not count a timed out sync. just retry + if err ~= "timeout" then + retry_count = retry_count + 1 + end + + -- in some cases, the new spawned timer will be switched to immediately, + -- preventing the coroutine who possesses the mutex to run + -- to let other coroutines has a chance to run + local ok, err = kong.timer:at(0.1, sync_once_impl, retry_count or 0) + -- this is a workaround for a timerng bug, where tail recursion causes failure + -- ok could be a string so let's convert it to boolean + if not ok then + return nil, err + end + return true +end + + +function _M:sync_once(delay) + local name = "rpc_sync_v2_once" + local is_managed = kong.timer:is_managed(name) + + -- we are running a sync handler + if is_managed then + return true + end + + local ok, err = kong.timer:named_at(name, delay or 0, sync_once_impl, 0) + if not ok then + return nil, err + end + + return true +end + + +function _M:sync_every(delay, stop) + local name = "rpc_sync_v2_every" + local is_managed = kong.timer:is_managed(name) + + -- we only start or stop once + + if stop then + if is_managed then + assert(kong.timer:cancel(name)) + end + return true + end + + if is_managed then + return true + end + + local ok, err = kong.timer:named_every(name, delay, sync_handler) + if not ok then + return nil, err + end + + return true +end + + +return _M diff --git a/tests/testdata/lua_kong_sync_retry_timeout/old.lua b/tests/testdata/lua_kong_sync_retry_timeout/old.lua new file mode 100644 index 0000000..4363254 --- /dev/null +++ b/tests/testdata/lua_kong_sync_retry_timeout/old.lua @@ -0,0 +1,469 @@ +local _M = {} +local _MT = { __index = _M, } + + +local txn = require("resty.lmdb.transaction") +local declarative = require("kong.db.declarative") +local constants = require("kong.constants") +local concurrency = require("kong.concurrency") +local isempty = require("table.isempty") +local events = require("kong.runloop.events") +local EMPTY = require("kong.tools.table").EMPTY + + +local insert_entity_for_txn = declarative.insert_entity_for_txn +local delete_entity_for_txn = declarative.delete_entity_for_txn +local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY +local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY +local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH +local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY +local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS +local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, } +local MAX_RETRY = 5 + + +local assert = assert +local ipairs = ipairs +local ngx_null = ngx.null +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_INFO = ngx.INFO +local ngx_DEBUG = ngx.DEBUG + + +function _M.new(strategy) + local self = { + strategy = strategy, + } + + return setmetatable(self, _MT) +end + + +local function empty_sync_result() + return { default = { deltas = {}, wipe = false, }, } +end + + +local function full_sync_result() + local deltas, err = declarative.export_config_sync() + if not deltas then + return nil, err + end + + -- wipe dp lmdb, full sync + return { default = { deltas = deltas, wipe = true, }, } +end + + +local function get_current_version() + return declarative.get_current_hash() or DECLARATIVE_EMPTY_CONFIG_HASH +end + + +function _M:init_cp(manager) + local purge_delay = manager.conf.cluster_data_plane_purge_delay + + -- CP + -- Method: kong.sync.v2.get_delta + -- Params: versions: list of current versions of the database + -- example: { default = { version = "1000", }, } + manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions) + ngx_log(ngx_DEBUG, "[kong.sync.v2] config push (connected client)") + + local rpc_peers + if kong.rpc then + rpc_peers = kong.rpc:get_peers() + end + + local default_namespace = current_versions.default + + if not default_namespace then + return nil, "default namespace does not exist inside params" + end + + -- { default = { version = "1000", }, } + local default_namespace_version = default_namespace.version + local node_info = assert(kong.rpc:get_peer_info(node_id)) + + -- follow update_sync_status() in control_plane.lua + local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, { + last_seen = ngx.time(), + hostname = node_id, + ip = node_info.ip, -- get the correct ip + version = node_info.version, -- get from rpc call + labels = node_info.labels, -- get from rpc call + cert_details = node_info.cert_details, -- get from rpc call + sync_status = CLUSTERING_SYNC_STATUS.NORMAL, + config_hash = default_namespace_version, + rpc_capabilities = rpc_peers and rpc_peers[node_id] or EMPTY, + }, { ttl = purge_delay, no_broadcast_crud_event = true, }) + if not ok then + ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err) + end + + local latest_version, err = self.strategy:get_latest_version() + if not latest_version then + return nil, err + end + + -- string comparison effectively does the same as number comparison + if not self.strategy:is_valid_version(default_namespace_version) or + default_namespace_version ~= latest_version then + return full_sync_result() + end + + return empty_sync_result() + end) +end + + +function _M:init_dp(manager) + local kong_shm = ngx.shared.kong + -- DP + -- Method: kong.sync.v2.notify_new_version + -- Params: new_versions: list of namespaces and their new versions, like: + -- { default = { new_version = "1000", }, } + manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions) + -- TODO: currently only default is supported, and anything else is ignored + local default_new_version = new_versions.default + if not default_new_version then + return nil, "default namespace does not exist inside params" + end + + local version = default_new_version.new_version + if not version then + return nil, "'new_version' key does not exist" + end + + local lmdb_ver = get_current_version() + if lmdb_ver < version then + -- set lastest version to shm + kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version) + return self:sync_once() + end + + ngx_log(ngx_DEBUG, "no sync runs, version is ", version) + + return true + end) +end + + +function _M:init(manager, is_cp) + if is_cp then + self:init_cp(manager) + else + self:init_dp(manager) + end +end + + +-- check if rpc connection is ready +local function is_rpc_ready() + -- TODO: find a better way to detect when the RPC layer, including caps list, + -- has been fully initialized, instead of waiting for up to 5.5 seconds + for i = 1, 10 do + local res = kong.rpc:get_peers() + + -- control_plane is ready + if res["control_plane"] then + return true + end + + -- retry later + ngx.sleep(0.1 * i) + end +end + + +local function do_sync() + if not is_rpc_ready() then + return nil, "rpc is not ready" + end + + local msg = { default = { version = get_current_version(), }, } + + local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg) + if not ns_deltas then + ngx_log(ngx_ERR, "sync get_delta error: ", err) + return true + end + + -- ns_deltas should look like: + -- { default = { deltas = { ... }, wipe = true, }, } + + local ns_delta = ns_deltas.default + if not ns_delta then + return nil, "default namespace does not exist inside params" + end + + local deltas = ns_delta.deltas + + if not deltas then + return nil, "sync get_delta error: deltas is null" + end + + if isempty(deltas) then + -- no delta to sync + return true + end + + -- we should find the correct default workspace + -- and replace the old one with it + local default_ws_changed + for _, delta in ipairs(deltas) do + if delta.type == "workspaces" and delta.entity.name == "default" and + kong.default_workspace ~= delta.entity.id + then + kong.default_workspace = delta.entity.id + default_ws_changed = true + break + end + end + assert(type(kong.default_workspace) == "string") + + local t = txn.begin(512) + + local wipe = ns_delta.wipe + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync begins") + + t:db_drop(false) + end + + local db = kong.db + + local version = "" + local opts = {} + local crud_events = {} + local crud_events_n = 0 + + -- delta should look like: + -- { type = ..., entity = { ... }, version = "1", ws_id = ..., } + for _, delta in ipairs(deltas) do + local delta_version = delta.version + local delta_type = delta.type + local delta_entity = delta.entity + local ev + + -- delta should have ws_id to generate the correct lmdb key + -- if entity is workspaceable + -- set the correct workspace for item + opts.workspace = delta.ws_id + + if delta_entity ~= nil and delta_entity ~= ngx_null then + -- upsert the entity + -- does the entity already exists? + local old_entity, err = db[delta_type]:select(delta_entity) + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + local res, err = insert_entity_for_txn(t, delta_type, delta_entity, opts) + if not res then + return nil, err + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] update entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, old_entity and "update" or "create", delta_entity, old_entity, } + end + + else + -- delete the entity, opts for getting correct lmdb key + local old_entity, err = db[delta_type]:select(delta.pk, opts) -- composite key + if err then + return nil, err + end + + -- If we will wipe lmdb, we don't need to delete it from lmdb. + if old_entity and not wipe then + local res, err = delete_entity_for_txn(t, delta_type, old_entity, opts) + if not res then + return nil, err + end + end + + ngx_log(ngx_DEBUG, + "[kong.sync.v2] delete entity", + ", version: ", delta_version, + ", type: ", delta_type) + + -- wipe the whole lmdb, should not have events + if not wipe then + ev = { delta_type, "delete", old_entity, } + end + end -- if delta_entity ~= nil and delta_entity ~= ngx_null + + -- wipe the whole lmdb, should not have events + if not wipe then + crud_events_n = crud_events_n + 1 + crud_events[crud_events_n] = ev + end + + -- delta.version should not be nil or ngx.null + assert(type(delta_version) == "string") + + if delta_version ~= version then + version = delta_version + end + end -- for _, delta + + -- store current sync version + t:set(DECLARATIVE_HASH_KEY, version) + + -- record the default workspace into LMDB for any of the following case: + -- * wipe is false, but the default workspace has been changed + -- * wipe is true (full sync) + if default_ws_changed or wipe then + t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace) + end + + local ok, err = t:commit() + if not ok then + return nil, err + end + + if wipe then + ngx_log(ngx_INFO, "[kong.sync.v2] full sync ends") + + kong.core_cache:purge() + kong.cache:purge() + + -- Trigger other workers' callbacks like reconfigure_handler. + -- + -- Full sync could rebuild route, plugins and balancer route, so their + -- hashes are nil. + local reconfigure_data = { kong.default_workspace, nil, nil, nil, } + local ok, err = events.declarative_reconfigure_notify(reconfigure_data) + if not ok then + return nil, err + end + + else + for _, event in ipairs(crud_events) do + -- delta_type, crud_event_type, delta.entity, old_entity + db[event[1]]:post_crud_event(event[2], event[3], event[4]) + end + end + + return true +end + + +local function sync_handler(premature) + if premature then + return + end + + local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync) + if not res and err ~= "timeout" then + ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err) + end +end + + +local sync_once_impl + + +local function start_sync_once_timer(retry_count) + local ok, err = kong.timer:at(0, sync_once_impl, retry_count or 0) + if not ok then + return nil, err + end + + return true +end + + +function sync_once_impl(premature, retry_count) + if premature then + return + end + + sync_handler() + + -- check if "kong.sync.v2.notify_new_version" updates the latest version + + local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY) + if not latest_notified_version then + ngx_log(ngx_DEBUG, "no version notified yet") + return + end + + local current_version = get_current_version() + if current_version >= latest_notified_version then + ngx_log(ngx_DEBUG, "version already updated") + return + end + + -- retry if the version is not updated + retry_count = retry_count or 0 + if retry_count > MAX_RETRY then + ngx_log(ngx_ERR, "sync_once retry count exceeded. retry_count: ", retry_count) + return + end + + return start_sync_once_timer(retry_count + 1) +end + + +function _M:sync_once(delay) + local name = "rpc_sync_v2_once" + local is_managed = kong.timer:is_managed(name) + + -- we are running a sync handler + if is_managed then + return true + end + + local ok, err = kong.timer:named_at(name, delay or 0, sync_once_impl, 0) + if not ok then + return nil, err + end + + return true +end + + +function _M:sync_every(delay, stop) + local name = "rpc_sync_v2_every" + local is_managed = kong.timer:is_managed(name) + + -- we only start or stop once + + if stop then + if is_managed then + assert(kong.timer:cancel(name)) + end + return true + end + + if is_managed then + return true + end + + local ok, err = kong.timer:named_every(name, delay, sync_handler) + if not ok then + return nil, err + end + + return true +end + + +return _M diff --git a/tests/testdata/lua_neovim_buf_func_scalar/expected.json b/tests/testdata/lua_neovim_buf_func_scalar/expected.json new file mode 100644 index 0000000..bc502bc --- /dev/null +++ b/tests/testdata/lua_neovim_buf_func_scalar/expected.json @@ -0,0 +1,1400 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 22 + ], + "type": "function_call", + "start_byte": 11278, + "end_byte": 11715 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 11716 + }, + "position": 22, + "subtree": true + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": -1, + "right_line": 327 + }, + { + "left_line": -1, + "right_line": 328 + }, + { + "left_line": -1, + "right_line": 329 + }, + { + "left_line": -1, + "right_line": 330 + }, + { + "left_line": -1, + "right_line": 331 + }, + { + "left_line": -1, + "right_line": 332 + }, + { + "left_line": -1, + "right_line": 333 + }, + { + "left_line": -1, + "right_line": 334 + }, + { + "left_line": -1, + "right_line": 335 + }, + { + "left_line": -1, + "right_line": 336 + }, + { + "left_line": -1, + "right_line": 337 + }, + { + "left_line": -1, + "right_line": 338 + }, + { + "left_line": -1, + "right_line": 339 + }, + { + "left_line": -1, + "right_line": 340 + }, + { + "left_line": -1, + "right_line": 341 + }, + { + "left_line": -1, + "right_line": 342 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_buf_func_scalar/metadata.json b/tests/testdata/lua_neovim_buf_func_scalar/metadata.json new file mode 100644 index 0000000..a442a6b --- /dev/null +++ b/tests/testdata/lua_neovim_buf_func_scalar/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "434792787b4c14423ca4003941f95039d3316076", + "commit": "30b4c1b4", + "message": "fix(ctx): missing options copy when loading hidden buffer #41149", + "author": "zeertzjq", + "date": "2026-08-04T16:50:36+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 16, + "deletions": 0 +} diff --git a/tests/testdata/lua_neovim_buf_func_scalar/new.lua b/tests/testdata/lua_neovim_buf_func_scalar/new.lua new file mode 100644 index 0000000..d04dc5a --- /dev/null +++ b/tests/testdata/lua_neovim_buf_func_scalar/new.lua @@ -0,0 +1,342 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local describe, it, before_each, after_each = t.describe, t.it, t.before_each, t.after_each +local eq = t.eq +local clear = n.clear +local fn = n.fn +local api = n.api +local command = n.command +local rmdir = n.rmdir +local pcall_err = t.pcall_err +local mkdir = t.mkdir + +local fname = 'Xtest-functional-eval-buf_functions' +local fname2 = fname .. '.2' +local dirname = fname .. '.d' + +before_each(clear) + +for _, func in ipairs({ + 'bufname(%s)', + 'bufnr(%s)', + 'bufwinnr(%s)', + 'getbufline(%s, 1)', + 'getbufvar(%s, "changedtick")', + 'setbufvar(%s, "f", 0)', +}) do + local funcname = func:match('%w+') + describe(funcname .. '() function', function() + it('errors out when receives v:true/v:false/v:null', function() + -- Not compatible with Vim: in Vim it always results in buffer not found + -- without any error messages. + for _, var in ipairs({ 'v:true', 'v:false' }) do + eq( + 'Vim(call):E5299: Expected a Number or a String, Boolean found', + pcall_err(command, 'call ' .. func:format(var)) + ) + end + eq( + 'Vim(call):E5300: Expected a Number or a String', + pcall_err(command, 'call ' .. func:format('v:null')) + ) + end) + it('errors out when receives invalid argument', function() + eq( + 'Vim(call):E745: Expected a Number or a String, List found', + pcall_err(command, 'call ' .. func:format('[]')) + ) + eq( + 'Vim(call):E728: Expected a Number or a String, Dictionary found', + pcall_err(command, 'call ' .. func:format('{}')) + ) + eq( + 'Vim(call):E805: Expected a Number or a String, Float found', + pcall_err(command, 'call ' .. func:format('0.0')) + ) + eq( + 'Vim(call):E703: Expected a Number or a String, Funcref found', + pcall_err(command, 'call ' .. func:format('function("tr")')) + ) + end) + end) +end + +describe('bufname() function', function() + it('returns empty string when buffer was not found', function() + command('file ' .. fname) + eq('', fn.bufname(2)) + eq('', fn.bufname('non-existent-buffer')) + eq('', fn.bufname('#')) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq('', fn.bufname('X')) + end) + before_each(function() + mkdir(dirname) + end) + after_each(function() + rmdir(dirname) + end) + it('returns expected buffer name', function() + eq('', fn.bufname('%')) -- Buffer has no name yet + command('file ' .. fname) + local wd = assert(t.fix_slashes(assert(vim.uv.cwd()))) + local sep = '/' + local curdirname = fn.fnamemodify(wd, ':t') + for _, arg in ipairs({ '%', 1, 'X', wd }) do + eq(fname, fn.bufname(arg)) + api.nvim_set_current_dir('..') + eq(curdirname .. sep .. fname, fn.bufname(arg)) + api.nvim_set_current_dir(curdirname) + api.nvim_set_current_dir(dirname) + eq(wd .. sep .. fname, fn.bufname(arg)) + api.nvim_set_current_dir('..') + eq(fname, fn.bufname(arg)) + command('enew') + end + eq('', fn.bufname('%')) + eq('', fn.bufname('$')) + eq(2, fn.bufnr('%')) + end) +end) + +describe('bufnr() function', function() + it('returns -1 when buffer was not found', function() + command('file ' .. fname) + eq(-1, fn.bufnr(2)) + eq(-1, fn.bufnr('non-existent-buffer')) + eq(-1, fn.bufnr('#')) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq(-1, fn.bufnr('X')) + end) + it('returns expected buffer number', function() + eq(1, fn.bufnr('%')) + command('file ' .. fname) + local wd = vim.uv.cwd() + local curdirname = fn.fnamemodify(wd, ':t') + eq(1, fn.bufnr(fname)) + eq(1, fn.bufnr(wd)) + eq(1, fn.bufnr(curdirname)) + eq(1, fn.bufnr('X')) + end) + it('returns number of last buffer with "$"', function() + eq(1, fn.bufnr('$')) + command('new') + eq(2, fn.bufnr('$')) + command('new') + eq(3, fn.bufnr('$')) + command('only') + eq(3, fn.bufnr('$')) + eq(3, fn.bufnr('%')) + command('buffer 1') + eq(3, fn.bufnr('$')) + eq(1, fn.bufnr('%')) + command('bwipeout 2') + eq(3, fn.bufnr('$')) + eq(1, fn.bufnr('%')) + command('bwipeout 3') + eq(1, fn.bufnr('$')) + eq(1, fn.bufnr('%')) + command('new') + eq(4, fn.bufnr('$')) + end) +end) + +describe('bufwinnr() function', function() + it('returns -1 when buffer was not found', function() + command('file ' .. fname) + eq(-1, fn.bufwinnr(2)) + eq(-1, fn.bufwinnr('non-existent-buffer')) + eq(-1, fn.bufwinnr('#')) + command('split ' .. fname2) -- It would be OK if there was one window + eq(2, fn.bufnr('%')) + eq(-1, fn.bufwinnr('X')) + end) + before_each(function() + mkdir(dirname) + end) + after_each(function() + rmdir(dirname) + end) + it('returns expected window number', function() + eq(1, fn.bufwinnr('%')) + command('file ' .. fname) + command('vsplit') + command('split ' .. fname2) + eq(2, fn.bufwinnr(fname)) + eq(1, fn.bufwinnr(fname2)) + eq(-1, fn.bufwinnr(fname:sub(1, #fname - 1))) + api.nvim_set_current_dir(dirname) + eq(2, fn.bufwinnr(fname)) + eq(1, fn.bufwinnr(fname2)) + eq(-1, fn.bufwinnr(fname:sub(1, #fname - 1))) + eq(1, fn.bufwinnr('%')) + eq(2, fn.bufwinnr(1)) + eq(1, fn.bufwinnr(2)) + eq(-1, fn.bufwinnr(3)) + eq(1, fn.bufwinnr('$')) + end) +end) + +describe('getbufline() function', function() + it('returns empty list when buffer was not found', function() + command('file ' .. fname) + eq({}, fn.getbufline(2, 1)) + eq({}, fn.getbufline('non-existent-buffer', 1)) + eq({}, fn.getbufline('#', 1)) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq({}, fn.getbufline('X', 1)) + end) + it('returns empty list when range is invalid', function() + eq({}, fn.getbufline(1, 0)) + eq({}, fn.getbufline(-1, 1, '$')) + eq({}, fn.getbufline(-1, '$', '$')) + api.nvim_buf_set_lines(0, 0, 1, false, { 'foo', 'bar', 'baz' }) + eq({}, fn.getbufline(1, 2, 1)) + eq({}, fn.getbufline(1, -10, -20)) + eq({}, fn.getbufline(1, -2, -1)) + eq({}, fn.getbufline(1, -1, 9999)) + eq({}, fn.getbufline(-1, 1, '$')) + eq({}, fn.getbufline(-1, '$', '$')) + end) + it('returns expected lines', function() + api.nvim_set_option_value('hidden', true, {}) + command('file ' .. fname) + api.nvim_buf_set_lines(0, 0, 1, false, { 'foo\0', '\0bar', 'baz' }) + command('edit ' .. fname2) + api.nvim_buf_set_lines(0, 0, 1, false, { 'abc\0', '\0def', 'ghi' }) + eq({ 'foo\n', '\nbar', 'baz' }, fn.getbufline(1, 1, 9999)) + eq({ 'abc\n', '\ndef', 'ghi' }, fn.getbufline(2, 1, 9999)) + eq({ 'foo\n', '\nbar', 'baz' }, fn.getbufline(1, 1, '$')) + eq({ 'baz' }, fn.getbufline(1, '$', '$')) + eq({ 'baz' }, fn.getbufline(1, '$', 9999)) + end) +end) + +describe('getbufvar() function', function() + it('returns empty list when buffer was not found', function() + command('file ' .. fname) + eq('', fn.getbufvar(2, '&autoindent')) + eq('', fn.getbufvar('non-existent-buffer', '&autoindent')) + eq('', fn.getbufvar('#', '&autoindent')) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq('', fn.getbufvar('X', '&autoindent')) + end) + it('returns empty list when variable/option/etc was not found', function() + command('file ' .. fname) + eq('', fn.getbufvar(1, '&autondent')) + eq('', fn.getbufvar(1, 'changedtic')) + end) + it('returns expected option value', function() + eq(0, fn.getbufvar(1, '&autoindent')) + eq(0, fn.getbufvar(1, '&l:autoindent')) + eq(0, fn.getbufvar(1, '&g:autoindent')) + -- Also works with global-only options + eq(1, fn.getbufvar(1, '&hidden')) + eq(1, fn.getbufvar(1, '&l:hidden')) + eq(1, fn.getbufvar(1, '&g:hidden')) + -- Also works with window-local options + eq(0, fn.getbufvar(1, '&number')) + eq(0, fn.getbufvar(1, '&l:number')) + eq(0, fn.getbufvar(1, '&g:number')) + command('new') + -- But with window-local options it probably does not what you expect + command('setl number') + -- (note that current window’s buffer is 2, but getbufvar() receives 1) + eq(2, api.nvim_win_get_buf(0)) + eq(1, fn.getbufvar(1, '&number')) + eq(1, fn.getbufvar(1, '&l:number')) + -- You can get global value though, if you find this useful. + eq(0, fn.getbufvar(1, '&g:number')) + end) + it('returns expected variable value', function() + eq(2, fn.getbufvar(1, 'changedtick')) + api.nvim_buf_set_lines(0, 0, 1, false, { 'abc\0', '\0def', 'ghi' }) + eq(3, fn.getbufvar(1, 'changedtick')) + api.nvim_buf_set_var(0, 'test', true) + eq(true, fn.getbufvar(1, 'test')) + eq({ test = true, changedtick = 3 }, fn.getbufvar(1, '')) + command('new') + eq(3, fn.getbufvar(1, 'changedtick')) + eq(true, fn.getbufvar(1, 'test')) + eq({ test = true, changedtick = 3 }, fn.getbufvar(1, '')) + end) +end) + +describe('setbufvar() function', function() + it('throws the error or ignores the input when buffer was not found', function() + command('file ' .. fname) + command('call setbufvar(2, "&autoindent", 0)') + eq( + 'Vim(call):E94: No matching buffer for non-existent-buffer', + pcall_err(command, 'call setbufvar("non-existent-buffer", "&autoindent", 0)') + ) + command('call setbufvar("#", "&autoindent", 0)') + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq( + 'Vim(call):E93: More than one match for X', + pcall_err(command, 'call setbufvar("X", "&autoindent", 0)') + ) + end) + it('may set options, including window-local and global values', function() + local buf1 = api.nvim_get_current_buf() + eq(false, api.nvim_get_option_value('number', {})) + command('split') + command('new') + eq(2, api.nvim_buf_get_number(api.nvim_win_get_buf(0))) + fn.setbufvar(1, '&number', true) + local windows = api.nvim_tabpage_list_wins(0) + eq(false, api.nvim_get_option_value('number', { win = windows[1] })) + eq(true, api.nvim_get_option_value('number', { win = windows[2] })) + eq(false, api.nvim_get_option_value('number', { win = windows[3] })) + eq(false, api.nvim_get_option_value('number', { win = api.nvim_get_current_win() })) + + eq(true, api.nvim_get_option_value('hidden', {})) + fn.setbufvar(1, '&hidden', 0) + eq(false, api.nvim_get_option_value('hidden', {})) + + eq(false, api.nvim_get_option_value('autoindent', { buf = buf1 })) + fn.setbufvar(1, '&autoindent', true) + eq(true, api.nvim_get_option_value('autoindent', { buf = buf1 })) + eq('Vim(call):E355: Unknown option: xxx', pcall_err(command, 'call setbufvar(1, "&xxx", 0)')) + end) + it('may set variables', function() + local buf1 = api.nvim_get_current_buf() + command('split') + command('new') + eq(2, api.nvim_buf_get_number(0)) + fn.setbufvar(1, 'number', true) + eq(true, api.nvim_buf_get_var(buf1, 'number')) + eq('Vim(call):E461: Illegal variable name: b:', pcall_err(command, 'call setbufvar(1, "", 0)')) + eq(true, api.nvim_buf_get_var(buf1, 'number')) + eq( + 'Vim:E46: Cannot change read-only variable "b:changedtick"', + pcall_err(fn.setbufvar, 1, 'changedtick', true) + ) + eq(2, fn.getbufvar(1, 'changedtick')) + end) + it('throws error when setting a string option to a boolean value vim-patch:9.0.0090', function() + eq('Vim:E928: String required', pcall_err(fn.setbufvar, '', '&errorformat', true)) + end) +end) + +describe('bufload() function', function() + before_each(function() + t.write_file(fname, 'foo\n') + end) + after_each(function() + os.remove(fname) + end) + it('does not set hidden buffer as modified #41148', function() + local buf = fn.bufadd(fname) + fn.bufload(buf) + api.nvim_command('edit ' .. fname) + eq(true, api.nvim_buf_is_loaded(buf)) + eq(false, api.nvim_get_option_value('modified', { buf = buf })) + end) +end) diff --git a/tests/testdata/lua_neovim_buf_func_scalar/old.lua b/tests/testdata/lua_neovim_buf_func_scalar/old.lua new file mode 100644 index 0000000..23262ac --- /dev/null +++ b/tests/testdata/lua_neovim_buf_func_scalar/old.lua @@ -0,0 +1,326 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local describe, it, before_each, after_each = t.describe, t.it, t.before_each, t.after_each +local eq = t.eq +local clear = n.clear +local fn = n.fn +local api = n.api +local command = n.command +local rmdir = n.rmdir +local pcall_err = t.pcall_err +local mkdir = t.mkdir + +local fname = 'Xtest-functional-eval-buf_functions' +local fname2 = fname .. '.2' +local dirname = fname .. '.d' + +before_each(clear) + +for _, func in ipairs({ + 'bufname(%s)', + 'bufnr(%s)', + 'bufwinnr(%s)', + 'getbufline(%s, 1)', + 'getbufvar(%s, "changedtick")', + 'setbufvar(%s, "f", 0)', +}) do + local funcname = func:match('%w+') + describe(funcname .. '() function', function() + it('errors out when receives v:true/v:false/v:null', function() + -- Not compatible with Vim: in Vim it always results in buffer not found + -- without any error messages. + for _, var in ipairs({ 'v:true', 'v:false' }) do + eq( + 'Vim(call):E5299: Expected a Number or a String, Boolean found', + pcall_err(command, 'call ' .. func:format(var)) + ) + end + eq( + 'Vim(call):E5300: Expected a Number or a String', + pcall_err(command, 'call ' .. func:format('v:null')) + ) + end) + it('errors out when receives invalid argument', function() + eq( + 'Vim(call):E745: Expected a Number or a String, List found', + pcall_err(command, 'call ' .. func:format('[]')) + ) + eq( + 'Vim(call):E728: Expected a Number or a String, Dictionary found', + pcall_err(command, 'call ' .. func:format('{}')) + ) + eq( + 'Vim(call):E805: Expected a Number or a String, Float found', + pcall_err(command, 'call ' .. func:format('0.0')) + ) + eq( + 'Vim(call):E703: Expected a Number or a String, Funcref found', + pcall_err(command, 'call ' .. func:format('function("tr")')) + ) + end) + end) +end + +describe('bufname() function', function() + it('returns empty string when buffer was not found', function() + command('file ' .. fname) + eq('', fn.bufname(2)) + eq('', fn.bufname('non-existent-buffer')) + eq('', fn.bufname('#')) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq('', fn.bufname('X')) + end) + before_each(function() + mkdir(dirname) + end) + after_each(function() + rmdir(dirname) + end) + it('returns expected buffer name', function() + eq('', fn.bufname('%')) -- Buffer has no name yet + command('file ' .. fname) + local wd = assert(t.fix_slashes(assert(vim.uv.cwd()))) + local sep = '/' + local curdirname = fn.fnamemodify(wd, ':t') + for _, arg in ipairs({ '%', 1, 'X', wd }) do + eq(fname, fn.bufname(arg)) + api.nvim_set_current_dir('..') + eq(curdirname .. sep .. fname, fn.bufname(arg)) + api.nvim_set_current_dir(curdirname) + api.nvim_set_current_dir(dirname) + eq(wd .. sep .. fname, fn.bufname(arg)) + api.nvim_set_current_dir('..') + eq(fname, fn.bufname(arg)) + command('enew') + end + eq('', fn.bufname('%')) + eq('', fn.bufname('$')) + eq(2, fn.bufnr('%')) + end) +end) + +describe('bufnr() function', function() + it('returns -1 when buffer was not found', function() + command('file ' .. fname) + eq(-1, fn.bufnr(2)) + eq(-1, fn.bufnr('non-existent-buffer')) + eq(-1, fn.bufnr('#')) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq(-1, fn.bufnr('X')) + end) + it('returns expected buffer number', function() + eq(1, fn.bufnr('%')) + command('file ' .. fname) + local wd = vim.uv.cwd() + local curdirname = fn.fnamemodify(wd, ':t') + eq(1, fn.bufnr(fname)) + eq(1, fn.bufnr(wd)) + eq(1, fn.bufnr(curdirname)) + eq(1, fn.bufnr('X')) + end) + it('returns number of last buffer with "$"', function() + eq(1, fn.bufnr('$')) + command('new') + eq(2, fn.bufnr('$')) + command('new') + eq(3, fn.bufnr('$')) + command('only') + eq(3, fn.bufnr('$')) + eq(3, fn.bufnr('%')) + command('buffer 1') + eq(3, fn.bufnr('$')) + eq(1, fn.bufnr('%')) + command('bwipeout 2') + eq(3, fn.bufnr('$')) + eq(1, fn.bufnr('%')) + command('bwipeout 3') + eq(1, fn.bufnr('$')) + eq(1, fn.bufnr('%')) + command('new') + eq(4, fn.bufnr('$')) + end) +end) + +describe('bufwinnr() function', function() + it('returns -1 when buffer was not found', function() + command('file ' .. fname) + eq(-1, fn.bufwinnr(2)) + eq(-1, fn.bufwinnr('non-existent-buffer')) + eq(-1, fn.bufwinnr('#')) + command('split ' .. fname2) -- It would be OK if there was one window + eq(2, fn.bufnr('%')) + eq(-1, fn.bufwinnr('X')) + end) + before_each(function() + mkdir(dirname) + end) + after_each(function() + rmdir(dirname) + end) + it('returns expected window number', function() + eq(1, fn.bufwinnr('%')) + command('file ' .. fname) + command('vsplit') + command('split ' .. fname2) + eq(2, fn.bufwinnr(fname)) + eq(1, fn.bufwinnr(fname2)) + eq(-1, fn.bufwinnr(fname:sub(1, #fname - 1))) + api.nvim_set_current_dir(dirname) + eq(2, fn.bufwinnr(fname)) + eq(1, fn.bufwinnr(fname2)) + eq(-1, fn.bufwinnr(fname:sub(1, #fname - 1))) + eq(1, fn.bufwinnr('%')) + eq(2, fn.bufwinnr(1)) + eq(1, fn.bufwinnr(2)) + eq(-1, fn.bufwinnr(3)) + eq(1, fn.bufwinnr('$')) + end) +end) + +describe('getbufline() function', function() + it('returns empty list when buffer was not found', function() + command('file ' .. fname) + eq({}, fn.getbufline(2, 1)) + eq({}, fn.getbufline('non-existent-buffer', 1)) + eq({}, fn.getbufline('#', 1)) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq({}, fn.getbufline('X', 1)) + end) + it('returns empty list when range is invalid', function() + eq({}, fn.getbufline(1, 0)) + eq({}, fn.getbufline(-1, 1, '$')) + eq({}, fn.getbufline(-1, '$', '$')) + api.nvim_buf_set_lines(0, 0, 1, false, { 'foo', 'bar', 'baz' }) + eq({}, fn.getbufline(1, 2, 1)) + eq({}, fn.getbufline(1, -10, -20)) + eq({}, fn.getbufline(1, -2, -1)) + eq({}, fn.getbufline(1, -1, 9999)) + eq({}, fn.getbufline(-1, 1, '$')) + eq({}, fn.getbufline(-1, '$', '$')) + end) + it('returns expected lines', function() + api.nvim_set_option_value('hidden', true, {}) + command('file ' .. fname) + api.nvim_buf_set_lines(0, 0, 1, false, { 'foo\0', '\0bar', 'baz' }) + command('edit ' .. fname2) + api.nvim_buf_set_lines(0, 0, 1, false, { 'abc\0', '\0def', 'ghi' }) + eq({ 'foo\n', '\nbar', 'baz' }, fn.getbufline(1, 1, 9999)) + eq({ 'abc\n', '\ndef', 'ghi' }, fn.getbufline(2, 1, 9999)) + eq({ 'foo\n', '\nbar', 'baz' }, fn.getbufline(1, 1, '$')) + eq({ 'baz' }, fn.getbufline(1, '$', '$')) + eq({ 'baz' }, fn.getbufline(1, '$', 9999)) + end) +end) + +describe('getbufvar() function', function() + it('returns empty list when buffer was not found', function() + command('file ' .. fname) + eq('', fn.getbufvar(2, '&autoindent')) + eq('', fn.getbufvar('non-existent-buffer', '&autoindent')) + eq('', fn.getbufvar('#', '&autoindent')) + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq('', fn.getbufvar('X', '&autoindent')) + end) + it('returns empty list when variable/option/etc was not found', function() + command('file ' .. fname) + eq('', fn.getbufvar(1, '&autondent')) + eq('', fn.getbufvar(1, 'changedtic')) + end) + it('returns expected option value', function() + eq(0, fn.getbufvar(1, '&autoindent')) + eq(0, fn.getbufvar(1, '&l:autoindent')) + eq(0, fn.getbufvar(1, '&g:autoindent')) + -- Also works with global-only options + eq(1, fn.getbufvar(1, '&hidden')) + eq(1, fn.getbufvar(1, '&l:hidden')) + eq(1, fn.getbufvar(1, '&g:hidden')) + -- Also works with window-local options + eq(0, fn.getbufvar(1, '&number')) + eq(0, fn.getbufvar(1, '&l:number')) + eq(0, fn.getbufvar(1, '&g:number')) + command('new') + -- But with window-local options it probably does not what you expect + command('setl number') + -- (note that current window’s buffer is 2, but getbufvar() receives 1) + eq(2, api.nvim_win_get_buf(0)) + eq(1, fn.getbufvar(1, '&number')) + eq(1, fn.getbufvar(1, '&l:number')) + -- You can get global value though, if you find this useful. + eq(0, fn.getbufvar(1, '&g:number')) + end) + it('returns expected variable value', function() + eq(2, fn.getbufvar(1, 'changedtick')) + api.nvim_buf_set_lines(0, 0, 1, false, { 'abc\0', '\0def', 'ghi' }) + eq(3, fn.getbufvar(1, 'changedtick')) + api.nvim_buf_set_var(0, 'test', true) + eq(true, fn.getbufvar(1, 'test')) + eq({ test = true, changedtick = 3 }, fn.getbufvar(1, '')) + command('new') + eq(3, fn.getbufvar(1, 'changedtick')) + eq(true, fn.getbufvar(1, 'test')) + eq({ test = true, changedtick = 3 }, fn.getbufvar(1, '')) + end) +end) + +describe('setbufvar() function', function() + it('throws the error or ignores the input when buffer was not found', function() + command('file ' .. fname) + command('call setbufvar(2, "&autoindent", 0)') + eq( + 'Vim(call):E94: No matching buffer for non-existent-buffer', + pcall_err(command, 'call setbufvar("non-existent-buffer", "&autoindent", 0)') + ) + command('call setbufvar("#", "&autoindent", 0)') + command('edit ' .. fname2) + eq(2, fn.bufnr('%')) + eq( + 'Vim(call):E93: More than one match for X', + pcall_err(command, 'call setbufvar("X", "&autoindent", 0)') + ) + end) + it('may set options, including window-local and global values', function() + local buf1 = api.nvim_get_current_buf() + eq(false, api.nvim_get_option_value('number', {})) + command('split') + command('new') + eq(2, api.nvim_buf_get_number(api.nvim_win_get_buf(0))) + fn.setbufvar(1, '&number', true) + local windows = api.nvim_tabpage_list_wins(0) + eq(false, api.nvim_get_option_value('number', { win = windows[1] })) + eq(true, api.nvim_get_option_value('number', { win = windows[2] })) + eq(false, api.nvim_get_option_value('number', { win = windows[3] })) + eq(false, api.nvim_get_option_value('number', { win = api.nvim_get_current_win() })) + + eq(true, api.nvim_get_option_value('hidden', {})) + fn.setbufvar(1, '&hidden', 0) + eq(false, api.nvim_get_option_value('hidden', {})) + + eq(false, api.nvim_get_option_value('autoindent', { buf = buf1 })) + fn.setbufvar(1, '&autoindent', true) + eq(true, api.nvim_get_option_value('autoindent', { buf = buf1 })) + eq('Vim(call):E355: Unknown option: xxx', pcall_err(command, 'call setbufvar(1, "&xxx", 0)')) + end) + it('may set variables', function() + local buf1 = api.nvim_get_current_buf() + command('split') + command('new') + eq(2, api.nvim_buf_get_number(0)) + fn.setbufvar(1, 'number', true) + eq(true, api.nvim_buf_get_var(buf1, 'number')) + eq('Vim(call):E461: Illegal variable name: b:', pcall_err(command, 'call setbufvar(1, "", 0)')) + eq(true, api.nvim_buf_get_var(buf1, 'number')) + eq( + 'Vim:E46: Cannot change read-only variable "b:changedtick"', + pcall_err(fn.setbufvar, 1, 'changedtick', true) + ) + eq(2, fn.getbufvar(1, 'changedtick')) + end) + it('throws error when setting a string option to a boolean value vim-patch:9.0.0090', function() + eq('Vim:E928: String required', pcall_err(fn.setbufvar, '', '&errorformat', true)) + end) +end) diff --git a/tests/testdata/lua_neovim_dir_spec_change/expected.json b/tests/testdata/lua_neovim_dir_spec_change/expected.json new file mode 100644 index 0000000..16db77c --- /dev/null +++ b/tests/testdata/lua_neovim_dir_spec_change/expected.json @@ -0,0 +1,3980 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32 + ], + "type": "for_statement", + "start_byte": 18599, + "end_byte": 18888 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2279, + "end_byte": 19384 + }, + "position": 32 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 0 + ], + "type": "for", + "label": "for", + "start_byte": 18599, + "end_byte": 18602 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32 + ], + "type": "for_statement", + "start_byte": 18599, + "end_byte": 19384 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1 + ], + "type": "for_generic_clause", + "start_byte": 18603, + "end_byte": 18880 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32 + ], + "type": "for_statement", + "start_byte": 18599, + "end_byte": 19384 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 2 + ], + "type": "do", + "label": "do", + "start_byte": 18881, + "end_byte": 18883 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32 + ], + "type": "for_statement", + "start_byte": 18599, + "end_byte": 19384 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 4 + ], + "type": "end", + "label": "end", + "start_byte": 19381, + "end_byte": 19384 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32 + ], + "type": "for_statement", + "start_byte": 18599, + "end_byte": 19384 + }, + "position": 4 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 0 + ], + "type": "identifier", + "label": "it", + "start_byte": 18599, + "end_byte": 18601 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0 + ], + "type": "function_call", + "start_byte": 18888, + "end_byte": 19378 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32 + ], + "type": "function_call", + "start_byte": 18599, + "end_byte": 18959 + }, + "old_position": 0, + "dest_start_byte": 18888, + "dest_end_byte": 18890, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 0 + ], + "type": "function", + "label": "function", + "start_byte": 18647, + "end_byte": 18655 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 18930, + "end_byte": 19377 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 18647, + "end_byte": 18958 + }, + "old_position": 0, + "dest_start_byte": 18930, + "dest_end_byte": 18938, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 1 + ], + "type": "parameters", + "start_byte": 18655, + "end_byte": 18657 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 18930, + "end_byte": 19377 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 18647, + "end_byte": 18958 + }, + "old_position": 1, + "dest_start_byte": 18938, + "dest_end_byte": 18940, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 0 + ], + "type": "if_statement", + "start_byte": 18662, + "end_byte": 18761 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 18947, + "dest_end_byte": 19050 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 1 + ], + "type": "function_call", + "start_byte": 18766, + "end_byte": 18780 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 19057, + "dest_end_byte": 19071, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 2 + ], + "type": "function_call", + "start_byte": 18785, + "end_byte": 18816 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 2, + "subtree": true, + "dest_start_byte": 19078, + "dest_end_byte": 19109, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 3 + ], + "type": "variable_declaration", + "start_byte": 18821, + "end_byte": 18844 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 3, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 3, + "subtree": true, + "dest_start_byte": 19116, + "dest_end_byte": 19139, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 4 + ], + "type": "function_call", + "start_byte": 18850, + "end_byte": 18858 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 4, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 4, + "subtree": true, + "dest_start_byte": 19147, + "dest_end_byte": 19155, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 5, + 0 + ], + "type": "identifier", + "label": "command", + "start_byte": 18863, + "end_byte": 18870 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2, + 5 + ], + "type": "function_call", + "start_byte": 19162, + "end_byte": 19196 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 5 + ], + "type": "function_call", + "start_byte": 18863, + "end_byte": 18887 + }, + "old_position": 0, + "dest_start_byte": 19162, + "dest_end_byte": 19169 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 6 + ], + "type": "function_call", + "start_byte": 18892, + "end_byte": 18899 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 6, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 6, + "subtree": true, + "dest_start_byte": 19203, + "dest_end_byte": 19210, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 7 + ], + "type": "function_call", + "start_byte": 18905, + "end_byte": 18927 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 9, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 7, + "subtree": true, + "dest_start_byte": 19320, + "dest_end_byte": 19342, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 8 + ], + "type": "function_call", + "start_byte": 18932, + "end_byte": 18952 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 10, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18662, + "end_byte": 18952 + }, + "old_position": 8, + "subtree": true, + "dest_start_byte": 19349, + "dest_end_byte": 19369, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 3 + ], + "type": "end", + "label": "end", + "start_byte": 18955, + "end_byte": 18958 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 18930, + "end_byte": 19377 + }, + "position": 3, + "old_parent": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 18647, + "end_byte": 18958 + }, + "old_position": 3, + "dest_start_byte": 19374, + "dest_end_byte": 19377 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 0 + ], + "type": "function_call", + "start_byte": 18891, + "end_byte": 18928 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1 + ], + "type": "arguments", + "start_byte": 18890, + "end_byte": 19378 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2, + 7 + ], + "type": "function_call", + "start_byte": 19218, + "end_byte": 19266 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 7, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2, + 8 + ], + "type": "function_call", + "start_byte": 19273, + "end_byte": 19313 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 18947, + "end_byte": 19369 + }, + "position": 8, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2, + 5, + 1 + ], + "type": "arguments", + "start_byte": 19169, + "end_byte": 19196 + }, + "parent": { + "tree": "after", + "path": [ + 25, + 1, + 1, + 2, + 32, + 3, + 0, + 1, + 1, + 2, + 5 + ], + "type": "function_call", + "start_byte": 19162, + "end_byte": 19196 + }, + "position": 1, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 0 + ], + "type": "string", + "label": "'supports the FileExplorer browse contract'", + "start_byte": 18602, + "end_byte": 18645 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 25, + 1, + 1, + 2, + 32, + 1, + 1, + 2, + 5, + 1 + ], + "type": "arguments", + "start_byte": 18870, + "end_byte": 18887 + }, + "subtree": true + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": 376, + "right_line": 376 + }, + { + "left_line": 377, + "right_line": 377 + }, + { + "left_line": 378, + "right_line": 378 + }, + { + "left_line": 379, + "right_line": 379 + }, + { + "left_line": 380, + "right_line": 380 + }, + { + "left_line": 381, + "right_line": 381 + }, + { + "left_line": 382, + "right_line": 382 + }, + { + "left_line": 383, + "right_line": 383 + }, + { + "left_line": 384, + "right_line": 384 + }, + { + "left_line": 385, + "right_line": 385 + }, + { + "left_line": 386, + "right_line": 386 + }, + { + "left_line": 387, + "right_line": 387 + }, + { + "left_line": 388, + "right_line": 388 + }, + { + "left_line": 389, + "right_line": 389 + }, + { + "left_line": 390, + "right_line": 390 + }, + { + "left_line": 391, + "right_line": 391 + }, + { + "left_line": 392, + "right_line": 392 + }, + { + "left_line": 393, + "right_line": 393 + }, + { + "left_line": 394, + "right_line": 394 + }, + { + "left_line": 395, + "right_line": 395 + }, + { + "left_line": 396, + "right_line": 396 + }, + { + "left_line": 397, + "right_line": 397 + }, + { + "left_line": 398, + "right_line": 398 + }, + { + "left_line": 399, + "right_line": 399 + }, + { + "left_line": 400, + "right_line": 400 + }, + { + "left_line": 401, + "right_line": 401 + }, + { + "left_line": 402, + "right_line": 402 + }, + { + "left_line": 403, + "right_line": 403 + }, + { + "left_line": 404, + "right_line": 404 + }, + { + "left_line": 405, + "right_line": 405 + }, + { + "left_line": 406, + "right_line": 406 + }, + { + "left_line": 407, + "right_line": 407 + }, + { + "left_line": 408, + "right_line": 408 + }, + { + "left_line": 409, + "right_line": 409 + }, + { + "left_line": 410, + "right_line": 410 + }, + { + "left_line": 411, + "right_line": 411 + }, + { + "left_line": 412, + "right_line": 412 + }, + { + "left_line": 413, + "right_line": 413 + }, + { + "left_line": 414, + "right_line": 414 + }, + { + "left_line": 415, + "right_line": 415 + }, + { + "left_line": 416, + "right_line": 416 + }, + { + "left_line": 417, + "right_line": 417 + }, + { + "left_line": 418, + "right_line": 418 + }, + { + "left_line": 419, + "right_line": 419 + }, + { + "left_line": 420, + "right_line": 420 + }, + { + "left_line": 421, + "right_line": 421 + }, + { + "left_line": 422, + "right_line": 422 + }, + { + "left_line": 423, + "right_line": 423 + }, + { + "left_line": 424, + "right_line": 424 + }, + { + "left_line": 425, + "right_line": 425 + }, + { + "left_line": 426, + "right_line": 426 + }, + { + "left_line": 427, + "right_line": 427 + }, + { + "left_line": 428, + "right_line": 428 + }, + { + "left_line": 429, + "right_line": 429 + }, + { + "left_line": 430, + "right_line": 430 + }, + { + "left_line": 431, + "right_line": 431 + }, + { + "left_line": 432, + "right_line": 432 + }, + { + "left_line": 433, + "right_line": 433 + }, + { + "left_line": 434, + "right_line": 434 + }, + { + "left_line": 435, + "right_line": 435 + }, + { + "left_line": 436, + "right_line": 436 + }, + { + "left_line": 437, + "right_line": 437 + }, + { + "left_line": 438, + "right_line": 438 + }, + { + "left_line": 439, + "right_line": 439 + }, + { + "left_line": 440, + "right_line": 440 + }, + { + "left_line": 441, + "right_line": 441 + }, + { + "left_line": 442, + "right_line": 442 + }, + { + "left_line": 443, + "right_line": 443 + }, + { + "left_line": 444, + "right_line": 444 + }, + { + "left_line": 445, + "right_line": 445 + }, + { + "left_line": 446, + "right_line": 446 + }, + { + "left_line": 447, + "right_line": 447 + }, + { + "left_line": 448, + "right_line": 448 + }, + { + "left_line": 449, + "right_line": 449 + }, + { + "left_line": 450, + "right_line": 450 + }, + { + "left_line": 451, + "right_line": 451 + }, + { + "left_line": 452, + "right_line": 452 + }, + { + "left_line": 453, + "right_line": 453 + }, + { + "left_line": 454, + "right_line": 454 + }, + { + "left_line": 455, + "right_line": 455 + }, + { + "left_line": 456, + "right_line": 456 + }, + { + "left_line": 457, + "right_line": 457 + }, + { + "left_line": 458, + "right_line": 458 + }, + { + "left_line": 459, + "right_line": 459 + }, + { + "left_line": 460, + "right_line": 460 + }, + { + "left_line": 461, + "right_line": 461 + }, + { + "left_line": 462, + "right_line": 462 + }, + { + "left_line": 463, + "right_line": 463 + }, + { + "left_line": 464, + "right_line": 464 + }, + { + "left_line": 465, + "right_line": 465 + }, + { + "left_line": 466, + "right_line": 466 + }, + { + "left_line": 467, + "right_line": 467 + }, + { + "left_line": 468, + "right_line": 468 + }, + { + "left_line": 469, + "right_line": 469 + }, + { + "left_line": 470, + "right_line": 470 + }, + { + "left_line": 471, + "right_line": 471 + }, + { + "left_line": 472, + "right_line": 472 + }, + { + "left_line": 473, + "right_line": 473 + }, + { + "left_line": 474, + "right_line": 474 + }, + { + "left_line": 475, + "right_line": 475 + }, + { + "left_line": 476, + "right_line": 476 + }, + { + "left_line": 477, + "right_line": 477 + }, + { + "left_line": 478, + "right_line": 478 + }, + { + "left_line": 479, + "right_line": 479 + }, + { + "left_line": 480, + "right_line": 480 + }, + { + "left_line": 481, + "right_line": 481 + }, + { + "left_line": 482, + "right_line": 482 + }, + { + "left_line": 483, + "right_line": 483 + }, + { + "left_line": 484, + "right_line": 484 + }, + { + "left_line": 485, + "right_line": 485 + }, + { + "left_line": 486, + "right_line": 486 + }, + { + "left_line": 487, + "right_line": 487 + }, + { + "left_line": 488, + "right_line": 488 + }, + { + "left_line": 489, + "right_line": 489 + }, + { + "left_line": 490, + "right_line": 490 + }, + { + "left_line": 491, + "right_line": 491 + }, + { + "left_line": 492, + "right_line": 492 + }, + { + "left_line": 493, + "right_line": 493 + }, + { + "left_line": 494, + "right_line": 494 + }, + { + "left_line": 495, + "right_line": 495 + }, + { + "left_line": 496, + "right_line": 496 + }, + { + "left_line": 497, + "right_line": 497 + }, + { + "left_line": 498, + "right_line": 498 + }, + { + "left_line": 499, + "right_line": 499 + }, + { + "left_line": 500, + "right_line": 500 + }, + { + "left_line": 501, + "right_line": 501 + }, + { + "left_line": 502, + "right_line": 502 + }, + { + "left_line": 503, + "right_line": 503 + }, + { + "left_line": 504, + "right_line": 504 + }, + { + "left_line": 505, + "right_line": 505 + }, + { + "left_line": 506, + "right_line": 506 + }, + { + "left_line": 507, + "right_line": 507 + }, + { + "left_line": 508, + "right_line": 508 + }, + { + "left_line": 509, + "right_line": 509 + }, + { + "left_line": 510, + "right_line": 510 + }, + { + "left_line": 511, + "right_line": 511 + }, + { + "left_line": 512, + "right_line": 512 + }, + { + "left_line": 513, + "right_line": 513 + }, + { + "left_line": 514, + "right_line": 514 + }, + { + "left_line": 515, + "right_line": 515 + }, + { + "left_line": 516, + "right_line": 516 + }, + { + "left_line": 517, + "right_line": 517 + }, + { + "left_line": 518, + "right_line": 518 + }, + { + "left_line": 519, + "right_line": 519 + }, + { + "left_line": 520, + "right_line": 520 + }, + { + "left_line": 521, + "right_line": 521 + }, + { + "left_line": 522, + "right_line": 522 + }, + { + "left_line": 523, + "right_line": 523 + }, + { + "left_line": 524, + "right_line": 524 + }, + { + "left_line": 525, + "right_line": 525 + }, + { + "left_line": 526, + "right_line": 526 + }, + { + "left_line": 527, + "right_line": 527 + }, + { + "left_line": 528, + "right_line": 528 + }, + { + "left_line": 529, + "right_line": 529 + }, + { + "left_line": 530, + "right_line": 530 + }, + { + "left_line": 531, + "right_line": 531 + }, + { + "left_line": 532, + "right_line": 532 + }, + { + "left_line": 533, + "right_line": 533 + }, + { + "left_line": 534, + "right_line": 534 + }, + { + "left_line": 535, + "right_line": 535 + }, + { + "left_line": 536, + "right_line": 536 + }, + { + "left_line": 537, + "right_line": 537 + }, + { + "left_line": 538, + "right_line": 538 + }, + { + "left_line": 539, + "right_line": 539 + }, + { + "left_line": 540, + "right_line": 540 + }, + { + "left_line": 541, + "right_line": 541 + }, + { + "left_line": 542, + "right_line": 542 + }, + { + "left_line": 543, + "right_line": 543 + }, + { + "left_line": 544, + "right_line": 544 + }, + { + "left_line": 545, + "right_line": 545 + }, + { + "left_line": 546, + "right_line": 546 + }, + { + "left_line": 547, + "right_line": 547 + }, + { + "left_line": 548, + "right_line": 548 + }, + { + "left_line": 549, + "right_line": 549 + }, + { + "left_line": 550, + "right_line": 550 + }, + { + "left_line": 551, + "right_line": 551 + }, + { + "left_line": 552, + "right_line": 552 + }, + { + "left_line": 553, + "right_line": 553 + }, + { + "left_line": 554, + "right_line": 554 + }, + { + "left_line": 555, + "right_line": 555 + }, + { + "left_line": 556, + "right_line": 556 + }, + { + "left_line": 557, + "right_line": 557 + }, + { + "left_line": 558, + "right_line": 558 + }, + { + "left_line": 559, + "right_line": 559 + }, + { + "left_line": 560, + "right_line": 560 + }, + { + "left_line": 561, + "right_line": 561 + }, + { + "left_line": 562, + "right_line": 562 + }, + { + "left_line": 563, + "right_line": 563 + }, + { + "left_line": 564, + "right_line": 564 + }, + { + "left_line": 565, + "right_line": 565 + }, + { + "left_line": 566, + "right_line": 566 + }, + { + "left_line": 567, + "right_line": 567 + }, + { + "left_line": 568, + "right_line": 568 + }, + { + "left_line": 569, + "right_line": 569 + }, + { + "left_line": 570, + "right_line": 570 + }, + { + "left_line": 571, + "right_line": 571 + }, + { + "left_line": 572, + "right_line": 572 + }, + { + "left_line": 573, + "right_line": 573 + }, + { + "left_line": 574, + "right_line": 574 + }, + { + "left_line": 575, + "right_line": 575 + }, + { + "left_line": 576, + "right_line": 576 + }, + { + "left_line": 577, + "right_line": 577 + }, + { + "left_line": 578, + "right_line": 578 + }, + { + "left_line": 579, + "right_line": 579 + }, + { + "left_line": 580, + "right_line": 580 + }, + { + "left_line": 581, + "right_line": 581 + }, + { + "left_line": 582, + "right_line": 582 + }, + { + "left_line": 583, + "right_line": 583 + }, + { + "left_line": 584, + "right_line": 584 + }, + { + "left_line": 585, + "right_line": 585 + }, + { + "left_line": 586, + "right_line": 586 + }, + { + "left_line": 587, + "right_line": 587 + }, + { + "left_line": 588, + "right_line": 588 + }, + { + "left_line": 589, + "right_line": 589 + }, + { + "left_line": 590, + "right_line": 590 + }, + { + "left_line": 591, + "right_line": 591 + }, + { + "left_line": 592, + "right_line": 592 + }, + { + "left_line": 593, + "right_line": 593 + }, + { + "left_line": 594, + "right_line": 594 + }, + { + "left_line": 595, + "right_line": 595 + }, + { + "left_line": 596, + "right_line": 596 + }, + { + "left_line": 597, + "right_line": 597 + }, + { + "left_line": 598, + "right_line": 598 + }, + { + "left_line": 599, + "right_line": 599 + }, + { + "left_line": 600, + "right_line": 600 + }, + { + "left_line": 601, + "right_line": 601 + }, + { + "left_line": 602, + "right_line": 602 + }, + { + "left_line": 603, + "right_line": 603 + }, + { + "left_line": 604, + "right_line": 604 + }, + { + "left_line": 605, + "right_line": 605 + }, + { + "left_line": 606, + "right_line": 606 + }, + { + "left_line": 607, + "right_line": 607 + }, + { + "left_line": 608, + "right_line": 608 + }, + { + "left_line": 609, + "right_line": 609 + }, + { + "left_line": 610, + "right_line": 610 + }, + { + "left_line": 611, + "right_line": 611 + }, + { + "left_line": 612, + "right_line": 612 + }, + { + "left_line": 613, + "right_line": 613 + }, + { + "left_line": 614, + "right_line": 614 + }, + { + "left_line": 615, + "right_line": 615 + }, + { + "left_line": 616, + "right_line": 616 + }, + { + "left_line": 617, + "right_line": 617 + }, + { + "left_line": 618, + "right_line": 618 + }, + { + "left_line": 619, + "right_line": 619 + }, + { + "left_line": 620, + "right_line": 620 + }, + { + "left_line": 621, + "right_line": 621 + }, + { + "left_line": 622, + "right_line": 622 + }, + { + "left_line": 623, + "right_line": 623 + }, + { + "left_line": 624, + "right_line": 624 + }, + { + "left_line": 625, + "right_line": 625 + }, + { + "left_line": 626, + "right_line": 626 + }, + { + "left_line": 627, + "right_line": 627 + }, + { + "left_line": 628, + "right_line": 628 + }, + { + "left_line": 629, + "right_line": 629 + }, + { + "left_line": 630, + "right_line": 630 + }, + { + "left_line": 631, + "right_line": 631 + }, + { + "left_line": 632, + "right_line": 632 + }, + { + "left_line": 633, + "right_line": 633 + }, + { + "left_line": 634, + "right_line": 634 + }, + { + "left_line": 635, + "right_line": 635 + }, + { + "left_line": 636, + "right_line": 636 + }, + { + "left_line": 637, + "right_line": 637 + }, + { + "left_line": 638, + "right_line": 638 + }, + { + "left_line": 639, + "right_line": 639 + }, + { + "left_line": 640, + "right_line": 640 + }, + { + "left_line": 641, + "right_line": 641 + }, + { + "left_line": 642, + "right_line": 642 + }, + { + "left_line": 643, + "right_line": 643 + }, + { + "left_line": 644, + "right_line": 644 + }, + { + "left_line": 645, + "right_line": 645 + }, + { + "left_line": 646, + "right_line": 646 + }, + { + "left_line": 647, + "right_line": 647 + }, + { + "left_line": 648, + "right_line": 648 + }, + { + "left_line": 649, + "right_line": 649 + }, + { + "left_line": 650, + "right_line": 650 + }, + { + "left_line": 651, + "right_line": 651 + }, + { + "left_line": 652, + "right_line": 652 + }, + { + "left_line": 653, + "right_line": 653 + }, + { + "left_line": 654, + "right_line": 654 + }, + { + "left_line": 655, + "right_line": 655 + }, + { + "left_line": 656, + "right_line": 656 + }, + { + "left_line": 657, + "right_line": 657 + }, + { + "left_line": 658, + "right_line": 658 + }, + { + "left_line": 659, + "right_line": 659 + }, + { + "left_line": 660, + "right_line": 660 + }, + { + "left_line": 661, + "right_line": 661 + }, + { + "left_line": 662, + "right_line": 662 + }, + { + "left_line": 663, + "right_line": 663 + }, + { + "left_line": 664, + "right_line": 664 + }, + { + "left_line": 665, + "right_line": 665 + }, + { + "left_line": 666, + "right_line": 666 + }, + { + "left_line": 667, + "right_line": 667 + }, + { + "left_line": 668, + "right_line": 668 + }, + { + "left_line": 669, + "right_line": 669 + }, + { + "left_line": 670, + "right_line": 670 + }, + { + "left_line": 671, + "right_line": 671 + }, + { + "left_line": 672, + "right_line": 672 + }, + { + "left_line": 673, + "right_line": 673 + }, + { + "left_line": 674, + "right_line": 674 + }, + { + "left_line": 675, + "right_line": 675 + }, + { + "left_line": 676, + "right_line": 676 + }, + { + "left_line": 677, + "right_line": 677 + }, + { + "left_line": 678, + "right_line": 678 + }, + { + "left_line": 679, + "right_line": 679 + }, + { + "left_line": 680, + "right_line": 680 + }, + { + "left_line": 681, + "right_line": 681 + }, + { + "left_line": 682, + "right_line": 682 + }, + { + "left_line": 683, + "right_line": 683 + }, + { + "left_line": 684, + "right_line": 684 + }, + { + "left_line": -1, + "right_line": 685 + }, + { + "left_line": -1, + "right_line": 686 + }, + { + "left_line": -1, + "right_line": 687 + }, + { + "left_line": -1, + "right_line": 688 + }, + { + "left_line": -1, + "right_line": 689 + }, + { + "left_line": -1, + "right_line": 690 + }, + { + "left_line": -1, + "right_line": 691 + }, + { + "left_line": 685, + "right_line": 692 + }, + { + "left_line": 686, + "right_line": 693 + }, + { + "left_line": 687, + "right_line": 694 + }, + { + "left_line": 688, + "right_line": 695 + }, + { + "left_line": 689, + "right_line": 696 + }, + { + "left_line": 690, + "right_line": 697 + }, + { + "left_line": 691, + "right_line": 698 + }, + { + "left_line": 692, + "right_line": 699 + }, + { + "left_line": 693, + "right_line": 700 + }, + { + "left_line": 694, + "right_line": 701 + }, + { + "left_line": 695, + "right_line": 702 + }, + { + "left_line": 696, + "right_line": 703 + }, + { + "left_line": -1, + "right_line": 704 + }, + { + "left_line": -1, + "right_line": 705 + }, + { + "left_line": 697, + "right_line": 706 + }, + { + "left_line": 698, + "right_line": 707 + }, + { + "left_line": 699, + "right_line": 708 + }, + { + "left_line": -1, + "right_line": 709 + }, + { + "left_line": 700, + "right_line": 710 + }, + { + "left_line": 701, + "right_line": 711 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_dir_spec_change/metadata.json b/tests/testdata/lua_neovim_dir_spec_change/metadata.json new file mode 100644 index 0000000..7fa0ce9 --- /dev/null +++ b/tests/testdata/lua_neovim_dir_spec_change/metadata.json @@ -0,0 +1,14 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "fb6fb33d39e376c22ddc2d44d782767b081d9229", + "commit": "4975a186", + "message": "fix(dir): `:browse edit` opens dir browser #40930", + "author": "Barrett Ruth", + "date": "2026-07-25T03:56:52-07:00", + "language": "lua", + "files_changed": 1, + "insertions": Solution: +23, + "deletions": Dispatch +13 +} diff --git a/tests/testdata/lua_neovim_dir_spec_change/new.lua b/tests/testdata/lua_neovim_dir_spec_change/new.lua new file mode 100644 index 0000000..7e6f43b --- /dev/null +++ b/tests/testdata/lua_neovim_dir_spec_change/new.lua @@ -0,0 +1,711 @@ +local n = require('test.functional.testnvim')() +local t = require('test.testutil') + +local describe, it, after_each, pending, finally = + t.describe, t.it, t.after_each, t.pending, t.finally +local api = n.api +local command = n.command +local eq = t.eq +local exec_capture = n.exec_capture +local exec_lua = n.exec_lua +local feed = n.feed +local fn = n.fn +local ok = t.ok +local poke_eventloop = n.poke_eventloop + +local function lines() + return api.nvim_buf_get_lines(0, 0, -1, true) +end + +local function edit(path) + api.nvim_cmd({ cmd = 'edit', args = { path }, magic = { file = false, bar = false } }, {}) +end + +local function cd(path) + api.nvim_cmd({ cmd = 'cd', args = { path }, magic = { file = false, bar = false } }, {}) +end + +local function line_of(text) + for i, line in ipairs(lines()) do + if line == text then + return i + end + end + error(('missing line %q in %s'):format(text, vim.inspect(lines()))) +end + +local function bufopt(name) + return api.nvim_get_option_value(name, { buf = 0 }) +end + +local function has_syntax_group(name) + return exec_capture('syntax list ' .. name):find(name, 1, true) ~= nil +end + +local function assert_directory(path) + local ffname = path:sub(-1) == '/' and path or path .. '/' + eq(ffname, api.nvim_buf_get_name(0)) + eq(path, vim.fs.normalize(fn.bufname('%'))) + eq('directory', bufopt('filetype')) + eq(true, bufopt('buflisted')) +end + +local function filesystem_root(path) + local root = vim.fs.normalize(vim.fs.abspath(path)) + for parent in vim.fs.parents(root) do + root = parent + end + return root +end + +local function write_config_plugin(path, text) + local plugin_file = vim.fs.joinpath(vim.fn.stdpath('config'), path) + vim.fs.mkdir(vim.fs.dirname(plugin_file), { parents = true }) + finally(function() + os.remove(plugin_file) + end) + t.write_file(plugin_file, text) +end + +---@param args? string[] +---@return string[] +local function with_buftype_optionset(args) + return vim.list_extend({ + '--cmd', + 'let g:nvim_dir_events = []', + '--cmd', + [[autocmd OptionSet buftype call add(g:nvim_dir_events, v:option_new)]], + }, args or {}) +end + +local function expect_buftype_optionset(path) + assert_directory(path) + eq({ 'nowrite' }, exec_lua('return vim.g.nvim_dir_events')) +end + +describe('nvim.dir', function() + local root + local subdir + local file + + local function make_fixture() + root = vim.fs.normalize(t.tmpname(false) .. ' space%#') + subdir = root .. '/subdir' + file = root .. '/alpha.txt' + t.mkdir(root) + t.mkdir(subdir) + t.write_file(file, 'alpha', true) + t.write_file(root .. '/.hidden', 'hidden', true) + end + + after_each(function() + if root then + n.rmdir(root) + end + root = nil + end) + + it('opens a startup directory argument', function() + make_fixture() + n.clear({ args_rm = { '-u' }, args = { root } }) + + assert_directory(root) + eq(false, vim.tbl_contains(lines(), '../')) + eq('subdir/', lines()[1]) + line_of('.hidden') + line_of('alpha.txt') + end) + + it('3P dir-browser can handle `FileType directory` event and rename buf', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + exec_lua(function() + _G.nvim_dir_loaded_in_filetype = nil + vim.api.nvim_create_autocmd('FileType', { + pattern = 'directory', + callback = function(ev) + _G.nvim_dir_loaded_in_filetype = package.loaded['nvim.dir'] ~= nil + local dir = vim.api.nvim_buf_get_name(ev.buf) + local browser = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(browser, 'example://' .. dir) + vim.api.nvim_buf_set_lines(browser, 0, -1, false, { + 'plugin browser for: ' .. dir, + }) + vim.api.nvim_set_current_buf(browser) + vim.api.nvim_buf_delete(ev.buf, { force = true }) + end, + }) + end) + + edit(root) + + eq(false, exec_lua('return _G.nvim_dir_loaded_in_filetype')) + eq('example://' .. root .. '/', api.nvim_buf_get_name(0)) + eq({ 'plugin browser for: ' .. root .. '/' }, lines()) + eq(false, exec_lua([[return package.loaded['nvim.dir'] ~= nil]])) + eq('', exec_capture('messages')) + end) + + it('triggers nested autocmds when opening directory buffers', function() + make_fixture() + + n.clear({ + args_rm = { '-u' }, + args = with_buftype_optionset({ root }), + }) + expect_buftype_optionset(root) + + n.clear({ + args_rm = { '-u' }, + args = with_buftype_optionset(), + }) + edit(root) + expect_buftype_optionset(root) + end) + + it('handles nested autocmds deleting the directory buffer', function() + make_fixture() + n.clear({ + args_rm = { '-u' }, + args = { + '--cmd', + 'let g:nvim_dir_wiped = 0', + '--cmd', + [[autocmd OptionSet buftype let g:nvim_dir_wiped = 1 | bwipeout!]], + root, + }, + }) + + eq(1, exec_lua('return vim.g.nvim_dir_wiped')) + eq('', exec_capture('messages')) + end) + + it('does not load the module until opening a directory', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + eq(false, exec_lua([[return package.loaded['nvim.dir'] ~= nil]])) + edit(root) + eq(true, exec_lua([[return package.loaded['nvim.dir'] ~= nil]])) + assert_directory(root) + end) + + it('opens a custom listing provider', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + require('nvim.dir').open(0, 'custom://root', { + list = function(_, name, cb) + vim.g.nvim_dir_list_name = name + cb(nil, { + { name = 'child', dir = true }, + { name = 'file.txt', dir = false }, + }) + end, + open = function(_, name, entry) + vim.g.nvim_dir_opened = entry.name .. ':' .. name + end, + open_parent = function(_, name) + vim.g.nvim_dir_parent = name + end, + init = function(buf) + vim.bo[buf].filetype = 'customdir' + end, + }) + end) + + eq('custom://root', api.nvim_buf_get_name(0)) + eq('customdir', bufopt('filetype')) + eq({ 'child/', 'file.txt' }, lines()) + eq('custom://root', exec_lua('return vim.g.nvim_dir_list_name')) + + api.nvim_win_set_cursor(0, { 2, 0 }) + api.nvim_set_option_value('modifiable', true, { buf = 0 }) + api.nvim_set_current_line('renamed.txt') + feed('') + poke_eventloop() + eq('renamed.txt:custom://root', exec_lua('return vim.g.nvim_dir_opened')) + + feed('-') + poke_eventloop() + eq('custom://root', exec_lua('return vim.g.nvim_dir_parent')) + end) + + it('reports custom listing provider errors', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + require('nvim.dir').open(0, 'custom://error', { + list = function(_, _, cb) + cb('simulated error') + end, + open = function() end, + open_parent = function() end, + }) + end) + + ok(exec_capture('messages'):find('simulated error', 1, true) ~= nil) + eq(false, exec_lua([[return vim.b.nvim_dir ~= nil]])) + end) + + it('reloads custom listing providers', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + require('nvim.dir').open(0, 'custom://root', { + list = function(buf, _, cb) + vim.b[buf].custom_count = (vim.b[buf].custom_count or 0) + 1 + cb(nil, { { name = 'file' .. vim.b[buf].custom_count .. '.txt', dir = false } }) + end, + open = function() end, + open_parent = function() end, + }) + end) + + eq({ 'file1.txt' }, lines()) + feed('R') + poke_eventloop() + eq({ 'file2.txt' }, lines()) + end) + + it('ignores callbacks from replaced listings', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + local dir = require('nvim.dir') + dir.open(0, 'custom://old', { + list = function(_, _, cb) + _G.nvim_dir_stale_callback = cb + end, + open = function() end, + open_parent = function() end, + }) + dir.open(0, 'custom://new', { + list = function(_, _, cb) + cb(nil, { { name = 'current.txt', dir = false } }) + end, + open = function() end, + open_parent = function() end, + }) + end) + + eq('custom://new', api.nvim_buf_get_name(0)) + eq({ 'current.txt' }, lines()) + + exec_lua(function() + _G.nvim_dir_stale_callback(nil, { { name = 'stale.txt', dir = false } }) + end) + + eq('custom://new', api.nvim_buf_get_name(0)) + eq({ 'current.txt' }, lines()) + end) + + it('replaces listing providers', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + local dir = require('nvim.dir') + local function provider(label) + return { + list = function(buf, _, cb) + local key = 'nvim_dir_' .. label .. '_lists' + vim.b[buf][key] = (vim.b[buf][key] or 0) + 1 + vim.g.nvim_dir_provider_list = label .. ':' .. vim.b[buf][key] + cb(nil, { { name = label .. '.txt', dir = false } }) + end, + open = function(_, _, entry) + vim.g.nvim_dir_provider_open = label .. ':' .. entry.name + end, + open_parent = function() + vim.g.nvim_dir_provider_parent = label + end, + } + end + dir.open(0, 'custom://old', provider('old')) + dir.open(0, 'custom://new', provider('new')) + end) + + eq({ 'new.txt' }, lines()) + eq('new:1', exec_lua('return vim.g.nvim_dir_provider_list')) + for _, plug in ipairs({ + '(nvim-dir-open)', + '(nvim-dir-up)', + '(nvim-dir-reload)', + }) do + eq(0, fn.maparg(plug, 'n', false, true).buffer) + end + + feed('') + poke_eventloop() + eq('new:new.txt', exec_lua('return vim.g.nvim_dir_provider_open')) + + feed('-') + poke_eventloop() + eq('new', exec_lua('return vim.g.nvim_dir_provider_parent')) + + feed('R') + poke_eventloop() + eq('new:2', exec_lua('return vim.g.nvim_dir_provider_list')) + end) + + it('maps [count]- to open parent directories', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + + edit(file) + feed('-') + poke_eventloop() + + assert_directory(root) + line_of('alpha.txt') + + -- Ensure the cursor stays on the entry we navigated up from. + eq('alpha.txt', api.nvim_get_current_line()) + + edit(file) + feed('1-') + poke_eventloop() + + assert_directory(root) + eq('alpha.txt', api.nvim_get_current_line()) + + edit(file) + feed('2-') + poke_eventloop() + + assert_directory(vim.fs.dirname(root)) + eq(vim.fs.basename(root) .. '/', api.nvim_get_current_line()) + end) + + it('maps - to open the current directory from an unnamed buffer', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + local cwd = fn.getcwd() + cd(root) + finally(function() + cd(cwd) + end) + + eq('', api.nvim_buf_get_name(0)) + feed('-') + poke_eventloop() + + assert_directory(root) + end) + + it('preserves a modified unnamed buffer when opening the current directory', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + local cwd = fn.getcwd() + cd(root) + finally(function() + cd(cwd) + end) + local old_buf = api.nvim_get_current_buf() + api.nvim_buf_set_lines(old_buf, 0, -1, false, { 'unsaved' }) + + feed('-') + poke_eventloop() + + assert_directory(root) + eq(old_buf, fn.bufnr('#')) + eq(true, api.nvim_buf_is_valid(old_buf)) + eq(true, api.nvim_get_option_value('modified', { buf = old_buf })) + eq({ 'unsaved' }, api.nvim_buf_get_lines(old_buf, 0, -1, false)) + end) + + it('does not shadow startup plugin `-` mappings in directory buffers', function() + make_fixture() + write_config_plugin( + 'plugin/dirvish.lua', + [[ + vim.g.dirvish_up = 0 + vim.keymap.set('n', '-', function() + vim.g.dirvish_up = vim.g.dirvish_up + 1 + end) + ]] + ) + + n.clear({ args_rm = { '-u', '--cmd' } }) + + eq(1, fn.exists('g:loaded_nvim_dir_plugin')) -- Avoid false negatives. + edit(root) + assert_directory(root) + eq(0, fn.maparg('-', 'n', false, true).buffer) + + feed('-') + poke_eventloop() + + eq(1, exec_lua('return vim.g.dirvish_up')) + assert_directory(root) + end) + + it('preserves alternate buffer when opening a parent directory', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + + edit(file) + feed('-') + poke_eventloop() + + assert_directory(root) + -- Keep the alternate buffer on the file we navigated up from. + eq(file, api.nvim_buf_get_name(fn.bufnr('#'))) + end) + + it('uses an absolute buffer name for a relative startup directory argument', function() + make_fixture() + local cwd = assert(vim.uv.cwd()) + assert(vim.uv.chdir(root)) + n.clear({ args_rm = { '-u' }, args = { '.' } }) + assert(vim.uv.chdir(cwd)) + + assert_directory(root) + end) + + it('normalizes edited directory names', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root .. '///') + + assert_directory(root) + end) + + it('does not show a parent entry at the filesystem root', function() + n.clear({ args_rm = { '-u' } }) + local root_dir = filesystem_root(fn.getcwd()) + + edit(root_dir) + + assert_directory(root_dir) + eq(false, vim.tbl_contains(lines(), '../')) + end) + + it('navigates entries and refreshes the listing', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root) + assert_directory(root) + + api.nvim_win_set_cursor(0, { line_of('alpha.txt'), 0 }) + feed('') + poke_eventloop() + eq(file, api.nvim_buf_get_name(0)) + eq({ 'alpha' }, lines()) + + edit(subdir) + assert_directory(subdir) + eq({ '' }, lines()) + + feed('-') + poke_eventloop() + assert_directory(root) + + t.write_file(root .. '/beta.txt', 'beta', true) + feed('R') + poke_eventloop() + line_of('beta.txt') + end) + + it("follows global 'hidden' when abandoned", function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + command('set hidden') + + edit(root) + local root_buf = api.nvim_get_current_buf() + eq('', bufopt('bufhidden')) + + edit(subdir) + eq(true, api.nvim_buf_is_loaded(root_buf)) + eq(1, fn.getbufinfo(root_buf)[1].hidden) + + n.clear({ args_rm = { '-u' } }) + command('set nohidden') + + edit(root) + root_buf = api.nvim_get_current_buf() + eq('', bufopt('bufhidden')) + + edit(subdir) + eq(false, api.nvim_buf_is_loaded(root_buf)) + end) + + it('reloads directory buffers', function() + make_fixture() + n.clear({ + args_rm = { '-u' }, + args = { '--clean', '--cmd', [[autocmd FileType directory ++once setlocal bufhidden=delete]] }, + }) + + edit(root) + assert_directory(root) + eq('delete', bufopt('bufhidden')) + eq(true, has_syntax_group('directoryDirectory')) + local buf = api.nvim_get_current_buf() + + t.write_file(root .. '/beta.txt', 'beta', true) + feed('R') + poke_eventloop() + eq('delete', bufopt('bufhidden')) + eq(true, has_syntax_group('directoryDirectory')) + line_of('beta.txt') + + t.write_file(root .. '/gamma.txt', 'gamma', true) + command('edit') + eq(buf, api.nvim_get_current_buf()) + assert_directory(root) + eq(true, has_syntax_group('directoryDirectory')) + line_of('subdir/') + line_of('alpha.txt') + line_of('gamma.txt') + end) + + it('reports an error and keeps the buffer when reloading a removed directory', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(subdir) + assert_directory(subdir) + + n.rmdir(subdir) + feed('R') + poke_eventloop() + + ok(exec_capture('messages'):find('ENOENT', 1, true) ~= nil) + assert_directory(subdir) + end) + + it('refreshes a directory when navigated into again', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root) + api.nvim_win_set_cursor(0, { line_of('subdir/'), 0 }) + feed('') + poke_eventloop() + assert_directory(subdir) + eq({ '' }, lines()) + + t.write_file(subdir .. '/new.txt', 'new', true) + feed('-') + poke_eventloop() + assert_directory(root) + + api.nvim_win_set_cursor(0, { line_of('subdir/'), 0 }) + feed('') + poke_eventloop() + assert_directory(subdir) + line_of('new.txt') + end) + + it('displays filenames as buffer text and opens them from the buffer', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root) + line_of('.hidden') + line_of('subdir/') + api.nvim_win_set_cursor(0, { line_of('alpha.txt'), 0 }) + feed('') + poke_eventloop() + + eq(file, api.nvim_buf_get_name(0)) + eq({ 'alpha' }, lines()) + end) + + it('encodes special filename characters in directory buffers', function() + -- Windows reserves backslash as a separator and disallows control characters in filenames. + -- https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file + t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') + make_fixture() + n.clear({ args_rm = { '-u' } }) + + local name = 'line\nbreak.txt' + local raw_names = { + 'back\\slash.txt', + 'tab\tname.txt', + 'carriage\rreturn.txt', + 'ctrl\1name.txt', + 'del\127name.txt', + } + for _, raw_name in ipairs(raw_names) do + t.write_file(root .. '/' .. raw_name, 'raw', true) + end + t.write_file(root .. '/' .. name, 'newline', true) + + edit(root) + for _, raw_name in ipairs(raw_names) do + line_of(raw_name) + end + api.nvim_win_set_cursor(0, { line_of('line\0break.txt'), 0 }) + feed('') + poke_eventloop() + + eq(root .. '/' .. name, api.nvim_buf_get_name(0)) + eq({ 'newline' }, lines()) + end) + + it('leaves existing special buffers alone', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + api.nvim_set_option_value('buftype', 'nofile', { buf = 0 }) + api.nvim_buf_set_name(0, root) + command('doautocmd BufEnter') + + eq('nofile', api.nvim_get_option_value('buftype', { buf = 0 })) + eq('', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('coexists with netrw and can be disabled', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + local cwd = fn.getcwd() + + ok(fn.exists(':Explore') > 0) + edit(root) + eq('directory', api.nvim_get_option_value('filetype', { buf = 0 })) + + cd(root) + command('Explore .') + cd(cwd) + eq('netrw', api.nvim_get_option_value('filetype', { buf = 0 })) + + n.clear({ + args_rm = { '-u' }, + args = { '--cmd', 'let g:loaded_nvim_dir_plugin = 1' }, + }) + edit(root) + eq('netrw', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + for _, case in ipairs({ + { command = 'edit', windows = 1, tabs = 1 }, + { command = 'split', windows = 2, tabs = 1 }, + { command = 'vsplit', windows = 2, tabs = 1 }, + { command = 'tabedit', windows = 1, tabs = 2 }, + { command = 'tabnew', windows = 1, tabs = 2 }, + }) do + it(('":browse %s"'):format(case.command), function() + if t.is_zig_build() then + return pending('broken with build.zig: TMPDIR relative cwd') + end + make_fixture() + n.clear({ args_rm = { '-u' } }) + local cwd = fn.getcwd() + + cd(root) + command('browse ' .. case.command) + cd(cwd) + + eq(case.windows, #api.nvim_tabpage_list_wins(0)) + eq(case.tabs, #api.nvim_list_tabpages()) + assert_directory(root) + line_of('alpha.txt') + end) + end +end) diff --git a/tests/testdata/lua_neovim_dir_spec_change/old.lua b/tests/testdata/lua_neovim_dir_spec_change/old.lua new file mode 100644 index 0000000..33212a4 --- /dev/null +++ b/tests/testdata/lua_neovim_dir_spec_change/old.lua @@ -0,0 +1,701 @@ +local n = require('test.functional.testnvim')() +local t = require('test.testutil') + +local describe, it, after_each, pending, finally = + t.describe, t.it, t.after_each, t.pending, t.finally +local api = n.api +local command = n.command +local eq = t.eq +local exec_capture = n.exec_capture +local exec_lua = n.exec_lua +local feed = n.feed +local fn = n.fn +local ok = t.ok +local poke_eventloop = n.poke_eventloop + +local function lines() + return api.nvim_buf_get_lines(0, 0, -1, true) +end + +local function edit(path) + api.nvim_cmd({ cmd = 'edit', args = { path }, magic = { file = false, bar = false } }, {}) +end + +local function cd(path) + api.nvim_cmd({ cmd = 'cd', args = { path }, magic = { file = false, bar = false } }, {}) +end + +local function line_of(text) + for i, line in ipairs(lines()) do + if line == text then + return i + end + end + error(('missing line %q in %s'):format(text, vim.inspect(lines()))) +end + +local function bufopt(name) + return api.nvim_get_option_value(name, { buf = 0 }) +end + +local function has_syntax_group(name) + return exec_capture('syntax list ' .. name):find(name, 1, true) ~= nil +end + +local function assert_directory(path) + local ffname = path:sub(-1) == '/' and path or path .. '/' + eq(ffname, api.nvim_buf_get_name(0)) + eq(path, vim.fs.normalize(fn.bufname('%'))) + eq('directory', bufopt('filetype')) + eq(true, bufopt('buflisted')) +end + +local function filesystem_root(path) + local root = vim.fs.normalize(vim.fs.abspath(path)) + for parent in vim.fs.parents(root) do + root = parent + end + return root +end + +local function write_config_plugin(path, text) + local plugin_file = vim.fs.joinpath(vim.fn.stdpath('config'), path) + vim.fs.mkdir(vim.fs.dirname(plugin_file), { parents = true }) + finally(function() + os.remove(plugin_file) + end) + t.write_file(plugin_file, text) +end + +---@param args? string[] +---@return string[] +local function with_buftype_optionset(args) + return vim.list_extend({ + '--cmd', + 'let g:nvim_dir_events = []', + '--cmd', + [[autocmd OptionSet buftype call add(g:nvim_dir_events, v:option_new)]], + }, args or {}) +end + +local function expect_buftype_optionset(path) + assert_directory(path) + eq({ 'nowrite' }, exec_lua('return vim.g.nvim_dir_events')) +end + +describe('nvim.dir', function() + local root + local subdir + local file + + local function make_fixture() + root = vim.fs.normalize(t.tmpname(false) .. ' space%#') + subdir = root .. '/subdir' + file = root .. '/alpha.txt' + t.mkdir(root) + t.mkdir(subdir) + t.write_file(file, 'alpha', true) + t.write_file(root .. '/.hidden', 'hidden', true) + end + + after_each(function() + if root then + n.rmdir(root) + end + root = nil + end) + + it('opens a startup directory argument', function() + make_fixture() + n.clear({ args_rm = { '-u' }, args = { root } }) + + assert_directory(root) + eq(false, vim.tbl_contains(lines(), '../')) + eq('subdir/', lines()[1]) + line_of('.hidden') + line_of('alpha.txt') + end) + + it('3P dir-browser can handle `FileType directory` event and rename buf', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + exec_lua(function() + _G.nvim_dir_loaded_in_filetype = nil + vim.api.nvim_create_autocmd('FileType', { + pattern = 'directory', + callback = function(ev) + _G.nvim_dir_loaded_in_filetype = package.loaded['nvim.dir'] ~= nil + local dir = vim.api.nvim_buf_get_name(ev.buf) + local browser = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(browser, 'example://' .. dir) + vim.api.nvim_buf_set_lines(browser, 0, -1, false, { + 'plugin browser for: ' .. dir, + }) + vim.api.nvim_set_current_buf(browser) + vim.api.nvim_buf_delete(ev.buf, { force = true }) + end, + }) + end) + + edit(root) + + eq(false, exec_lua('return _G.nvim_dir_loaded_in_filetype')) + eq('example://' .. root .. '/', api.nvim_buf_get_name(0)) + eq({ 'plugin browser for: ' .. root .. '/' }, lines()) + eq(false, exec_lua([[return package.loaded['nvim.dir'] ~= nil]])) + eq('', exec_capture('messages')) + end) + + it('triggers nested autocmds when opening directory buffers', function() + make_fixture() + + n.clear({ + args_rm = { '-u' }, + args = with_buftype_optionset({ root }), + }) + expect_buftype_optionset(root) + + n.clear({ + args_rm = { '-u' }, + args = with_buftype_optionset(), + }) + edit(root) + expect_buftype_optionset(root) + end) + + it('handles nested autocmds deleting the directory buffer', function() + make_fixture() + n.clear({ + args_rm = { '-u' }, + args = { + '--cmd', + 'let g:nvim_dir_wiped = 0', + '--cmd', + [[autocmd OptionSet buftype let g:nvim_dir_wiped = 1 | bwipeout!]], + root, + }, + }) + + eq(1, exec_lua('return vim.g.nvim_dir_wiped')) + eq('', exec_capture('messages')) + end) + + it('does not load the module until opening a directory', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + eq(false, exec_lua([[return package.loaded['nvim.dir'] ~= nil]])) + edit(root) + eq(true, exec_lua([[return package.loaded['nvim.dir'] ~= nil]])) + assert_directory(root) + end) + + it('opens a custom listing provider', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + require('nvim.dir').open(0, 'custom://root', { + list = function(_, name, cb) + vim.g.nvim_dir_list_name = name + cb(nil, { + { name = 'child', dir = true }, + { name = 'file.txt', dir = false }, + }) + end, + open = function(_, name, entry) + vim.g.nvim_dir_opened = entry.name .. ':' .. name + end, + open_parent = function(_, name) + vim.g.nvim_dir_parent = name + end, + init = function(buf) + vim.bo[buf].filetype = 'customdir' + end, + }) + end) + + eq('custom://root', api.nvim_buf_get_name(0)) + eq('customdir', bufopt('filetype')) + eq({ 'child/', 'file.txt' }, lines()) + eq('custom://root', exec_lua('return vim.g.nvim_dir_list_name')) + + api.nvim_win_set_cursor(0, { 2, 0 }) + api.nvim_set_option_value('modifiable', true, { buf = 0 }) + api.nvim_set_current_line('renamed.txt') + feed('') + poke_eventloop() + eq('renamed.txt:custom://root', exec_lua('return vim.g.nvim_dir_opened')) + + feed('-') + poke_eventloop() + eq('custom://root', exec_lua('return vim.g.nvim_dir_parent')) + end) + + it('reports custom listing provider errors', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + require('nvim.dir').open(0, 'custom://error', { + list = function(_, _, cb) + cb('simulated error') + end, + open = function() end, + open_parent = function() end, + }) + end) + + ok(exec_capture('messages'):find('simulated error', 1, true) ~= nil) + eq(false, exec_lua([[return vim.b.nvim_dir ~= nil]])) + end) + + it('reloads custom listing providers', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + require('nvim.dir').open(0, 'custom://root', { + list = function(buf, _, cb) + vim.b[buf].custom_count = (vim.b[buf].custom_count or 0) + 1 + cb(nil, { { name = 'file' .. vim.b[buf].custom_count .. '.txt', dir = false } }) + end, + open = function() end, + open_parent = function() end, + }) + end) + + eq({ 'file1.txt' }, lines()) + feed('R') + poke_eventloop() + eq({ 'file2.txt' }, lines()) + end) + + it('ignores callbacks from replaced listings', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + local dir = require('nvim.dir') + dir.open(0, 'custom://old', { + list = function(_, _, cb) + _G.nvim_dir_stale_callback = cb + end, + open = function() end, + open_parent = function() end, + }) + dir.open(0, 'custom://new', { + list = function(_, _, cb) + cb(nil, { { name = 'current.txt', dir = false } }) + end, + open = function() end, + open_parent = function() end, + }) + end) + + eq('custom://new', api.nvim_buf_get_name(0)) + eq({ 'current.txt' }, lines()) + + exec_lua(function() + _G.nvim_dir_stale_callback(nil, { { name = 'stale.txt', dir = false } }) + end) + + eq('custom://new', api.nvim_buf_get_name(0)) + eq({ 'current.txt' }, lines()) + end) + + it('replaces listing providers', function() + n.clear({ args_rm = { '-u' } }) + + exec_lua(function() + local dir = require('nvim.dir') + local function provider(label) + return { + list = function(buf, _, cb) + local key = 'nvim_dir_' .. label .. '_lists' + vim.b[buf][key] = (vim.b[buf][key] or 0) + 1 + vim.g.nvim_dir_provider_list = label .. ':' .. vim.b[buf][key] + cb(nil, { { name = label .. '.txt', dir = false } }) + end, + open = function(_, _, entry) + vim.g.nvim_dir_provider_open = label .. ':' .. entry.name + end, + open_parent = function() + vim.g.nvim_dir_provider_parent = label + end, + } + end + dir.open(0, 'custom://old', provider('old')) + dir.open(0, 'custom://new', provider('new')) + end) + + eq({ 'new.txt' }, lines()) + eq('new:1', exec_lua('return vim.g.nvim_dir_provider_list')) + for _, plug in ipairs({ + '(nvim-dir-open)', + '(nvim-dir-up)', + '(nvim-dir-reload)', + }) do + eq(0, fn.maparg(plug, 'n', false, true).buffer) + end + + feed('') + poke_eventloop() + eq('new:new.txt', exec_lua('return vim.g.nvim_dir_provider_open')) + + feed('-') + poke_eventloop() + eq('new', exec_lua('return vim.g.nvim_dir_provider_parent')) + + feed('R') + poke_eventloop() + eq('new:2', exec_lua('return vim.g.nvim_dir_provider_list')) + end) + + it('maps [count]- to open parent directories', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + + edit(file) + feed('-') + poke_eventloop() + + assert_directory(root) + line_of('alpha.txt') + + -- Ensure the cursor stays on the entry we navigated up from. + eq('alpha.txt', api.nvim_get_current_line()) + + edit(file) + feed('1-') + poke_eventloop() + + assert_directory(root) + eq('alpha.txt', api.nvim_get_current_line()) + + edit(file) + feed('2-') + poke_eventloop() + + assert_directory(vim.fs.dirname(root)) + eq(vim.fs.basename(root) .. '/', api.nvim_get_current_line()) + end) + + it('maps - to open the current directory from an unnamed buffer', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + local cwd = fn.getcwd() + cd(root) + finally(function() + cd(cwd) + end) + + eq('', api.nvim_buf_get_name(0)) + feed('-') + poke_eventloop() + + assert_directory(root) + end) + + it('preserves a modified unnamed buffer when opening the current directory', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + local cwd = fn.getcwd() + cd(root) + finally(function() + cd(cwd) + end) + local old_buf = api.nvim_get_current_buf() + api.nvim_buf_set_lines(old_buf, 0, -1, false, { 'unsaved' }) + + feed('-') + poke_eventloop() + + assert_directory(root) + eq(old_buf, fn.bufnr('#')) + eq(true, api.nvim_buf_is_valid(old_buf)) + eq(true, api.nvim_get_option_value('modified', { buf = old_buf })) + eq({ 'unsaved' }, api.nvim_buf_get_lines(old_buf, 0, -1, false)) + end) + + it('does not shadow startup plugin `-` mappings in directory buffers', function() + make_fixture() + write_config_plugin( + 'plugin/dirvish.lua', + [[ + vim.g.dirvish_up = 0 + vim.keymap.set('n', '-', function() + vim.g.dirvish_up = vim.g.dirvish_up + 1 + end) + ]] + ) + + n.clear({ args_rm = { '-u', '--cmd' } }) + + eq(1, fn.exists('g:loaded_nvim_dir_plugin')) -- Avoid false negatives. + edit(root) + assert_directory(root) + eq(0, fn.maparg('-', 'n', false, true).buffer) + + feed('-') + poke_eventloop() + + eq(1, exec_lua('return vim.g.dirvish_up')) + assert_directory(root) + end) + + it('preserves alternate buffer when opening a parent directory', function() + make_fixture() + n.clear({ args_rm = { '-u', '--cmd' } }) + + edit(file) + feed('-') + poke_eventloop() + + assert_directory(root) + -- Keep the alternate buffer on the file we navigated up from. + eq(file, api.nvim_buf_get_name(fn.bufnr('#'))) + end) + + it('uses an absolute buffer name for a relative startup directory argument', function() + make_fixture() + local cwd = assert(vim.uv.cwd()) + assert(vim.uv.chdir(root)) + n.clear({ args_rm = { '-u' }, args = { '.' } }) + assert(vim.uv.chdir(cwd)) + + assert_directory(root) + end) + + it('normalizes edited directory names', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root .. '///') + + assert_directory(root) + end) + + it('does not show a parent entry at the filesystem root', function() + n.clear({ args_rm = { '-u' } }) + local root_dir = filesystem_root(fn.getcwd()) + + edit(root_dir) + + assert_directory(root_dir) + eq(false, vim.tbl_contains(lines(), '../')) + end) + + it('navigates entries and refreshes the listing', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root) + assert_directory(root) + + api.nvim_win_set_cursor(0, { line_of('alpha.txt'), 0 }) + feed('') + poke_eventloop() + eq(file, api.nvim_buf_get_name(0)) + eq({ 'alpha' }, lines()) + + edit(subdir) + assert_directory(subdir) + eq({ '' }, lines()) + + feed('-') + poke_eventloop() + assert_directory(root) + + t.write_file(root .. '/beta.txt', 'beta', true) + feed('R') + poke_eventloop() + line_of('beta.txt') + end) + + it("follows global 'hidden' when abandoned", function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + command('set hidden') + + edit(root) + local root_buf = api.nvim_get_current_buf() + eq('', bufopt('bufhidden')) + + edit(subdir) + eq(true, api.nvim_buf_is_loaded(root_buf)) + eq(1, fn.getbufinfo(root_buf)[1].hidden) + + n.clear({ args_rm = { '-u' } }) + command('set nohidden') + + edit(root) + root_buf = api.nvim_get_current_buf() + eq('', bufopt('bufhidden')) + + edit(subdir) + eq(false, api.nvim_buf_is_loaded(root_buf)) + end) + + it('reloads directory buffers', function() + make_fixture() + n.clear({ + args_rm = { '-u' }, + args = { '--clean', '--cmd', [[autocmd FileType directory ++once setlocal bufhidden=delete]] }, + }) + + edit(root) + assert_directory(root) + eq('delete', bufopt('bufhidden')) + eq(true, has_syntax_group('directoryDirectory')) + local buf = api.nvim_get_current_buf() + + t.write_file(root .. '/beta.txt', 'beta', true) + feed('R') + poke_eventloop() + eq('delete', bufopt('bufhidden')) + eq(true, has_syntax_group('directoryDirectory')) + line_of('beta.txt') + + t.write_file(root .. '/gamma.txt', 'gamma', true) + command('edit') + eq(buf, api.nvim_get_current_buf()) + assert_directory(root) + eq(true, has_syntax_group('directoryDirectory')) + line_of('subdir/') + line_of('alpha.txt') + line_of('gamma.txt') + end) + + it('reports an error and keeps the buffer when reloading a removed directory', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(subdir) + assert_directory(subdir) + + n.rmdir(subdir) + feed('R') + poke_eventloop() + + ok(exec_capture('messages'):find('ENOENT', 1, true) ~= nil) + assert_directory(subdir) + end) + + it('refreshes a directory when navigated into again', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root) + api.nvim_win_set_cursor(0, { line_of('subdir/'), 0 }) + feed('') + poke_eventloop() + assert_directory(subdir) + eq({ '' }, lines()) + + t.write_file(subdir .. '/new.txt', 'new', true) + feed('-') + poke_eventloop() + assert_directory(root) + + api.nvim_win_set_cursor(0, { line_of('subdir/'), 0 }) + feed('') + poke_eventloop() + assert_directory(subdir) + line_of('new.txt') + end) + + it('displays filenames as buffer text and opens them from the buffer', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + edit(root) + line_of('.hidden') + line_of('subdir/') + api.nvim_win_set_cursor(0, { line_of('alpha.txt'), 0 }) + feed('') + poke_eventloop() + + eq(file, api.nvim_buf_get_name(0)) + eq({ 'alpha' }, lines()) + end) + + it('encodes special filename characters in directory buffers', function() + -- Windows reserves backslash as a separator and disallows control characters in filenames. + -- https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file + t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') + make_fixture() + n.clear({ args_rm = { '-u' } }) + + local name = 'line\nbreak.txt' + local raw_names = { + 'back\\slash.txt', + 'tab\tname.txt', + 'carriage\rreturn.txt', + 'ctrl\1name.txt', + 'del\127name.txt', + } + for _, raw_name in ipairs(raw_names) do + t.write_file(root .. '/' .. raw_name, 'raw', true) + end + t.write_file(root .. '/' .. name, 'newline', true) + + edit(root) + for _, raw_name in ipairs(raw_names) do + line_of(raw_name) + end + api.nvim_win_set_cursor(0, { line_of('line\0break.txt'), 0 }) + feed('') + poke_eventloop() + + eq(root .. '/' .. name, api.nvim_buf_get_name(0)) + eq({ 'newline' }, lines()) + end) + + it('leaves existing special buffers alone', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + + api.nvim_set_option_value('buftype', 'nofile', { buf = 0 }) + api.nvim_buf_set_name(0, root) + command('doautocmd BufEnter') + + eq('nofile', api.nvim_get_option_value('buftype', { buf = 0 })) + eq('', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('coexists with netrw and can be disabled', function() + make_fixture() + n.clear({ args_rm = { '-u' } }) + local cwd = fn.getcwd() + + ok(fn.exists(':Explore') > 0) + edit(root) + eq('directory', api.nvim_get_option_value('filetype', { buf = 0 })) + + cd(root) + command('Explore .') + cd(cwd) + eq('netrw', api.nvim_get_option_value('filetype', { buf = 0 })) + + n.clear({ + args_rm = { '-u' }, + args = { '--cmd', 'let g:loaded_nvim_dir_plugin = 1' }, + }) + edit(root) + eq('netrw', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('supports the FileExplorer browse contract', function() + if t.is_zig_build() then + return pending('broken with build.zig: TMPDIR relative cwd') + end + make_fixture() + n.clear({ args_rm = { '-u' } }) + local cwd = fn.getcwd() + + cd(root) + command('browse edit .') + cd(cwd) + + assert_directory(root) + line_of('alpha.txt') + end) +end) diff --git a/tests/testdata/lua_neovim_fs_api_refactor/expected.json b/tests/testdata/lua_neovim_fs_api_refactor/expected.json new file mode 100644 index 0000000..0e73c94 --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_refactor/expected.json @@ -0,0 +1,5557 @@ +{ + "version": "v1", + "actions": [ + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 23, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 6107, + "end_byte": 6112 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 3 + ], + "type": "variable_declaration", + "start_byte": 4952, + "end_byte": 4985 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3, + 23 + ], + "type": "variable_declaration", + "start_byte": 6107, + "end_byte": 6133 + }, + "old_position": 0, + "dest_start_byte": 4952, + "dest_end_byte": 4957, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 4 + ], + "type": "comment", + "start_byte": 5028, + "end_byte": 5110 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 7, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 4, + "dest_start_byte": 5142, + "dest_end_byte": 5167, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 8 + ], + "type": "comment", + "start_byte": 5209, + "end_byte": 5296 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 10, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 8, + "dest_start_byte": 5368, + "dest_end_byte": 5418, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 11 + ], + "type": "comment", + "start_byte": 5460, + "end_byte": 5515 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 13, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 11, + "dest_start_byte": 5644, + "dest_end_byte": 5698, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 13 + ], + "type": "comment", + "start_byte": 5683, + "end_byte": 5737 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 16, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 13, + "dest_start_byte": 5766, + "dest_end_byte": 5812, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 16 + ], + "type": "comment", + "start_byte": 5805, + "end_byte": 5851 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 18, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 16, + "dest_start_byte": 5839, + "dest_end_byte": 5887, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 18 + ], + "type": "comment", + "start_byte": 5878, + "end_byte": 5926 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 20, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 18, + "dest_start_byte": 5937, + "dest_end_byte": 5984, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 20 + ], + "type": "comment", + "start_byte": 5976, + "end_byte": 6023 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 22, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 20, + "dest_start_byte": 6031, + "dest_end_byte": 6065, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 22 + ], + "type": "comment", + "start_byte": 6070, + "end_byte": 6104 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 24, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 22, + "dest_start_byte": 6140, + "dest_end_byte": 6168, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 25 + ], + "type": "comment", + "start_byte": 6208, + "end_byte": 6236 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 26, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 25, + "dest_start_byte": 6198, + "dest_end_byte": 6235, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 27 + ], + "type": "comment", + "start_byte": 6266, + "end_byte": 6303 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 30, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 27, + "dest_start_byte": 6466, + "dest_end_byte": 6500, + "group_id": "group-2" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 31 + ], + "type": "comment", + "start_byte": 6524, + "end_byte": 6558 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 32, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7577 + }, + "old_position": 31, + "dest_start_byte": 6956, + "dest_end_byte": 7039, + "group_id": "group-2" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 4 + ], + "type": "comment", + "start_byte": 4989, + "end_byte": 5071 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3 + ], + "type": "block", + "start_byte": 4758, + "end_byte": 7543 + }, + "position": 4 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 3, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 5004, + "end_byte": 5005 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 3, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 4965, + "end_byte": 4966 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 4958, + "end_byte": 4985 + }, + "position": 1 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 3, + 2 + ], + "type": "expression_list", + "start_byte": 5006, + "end_byte": 5024 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 4958, + "end_byte": 4985 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3, + 3 + ], + "type": "assignment_statement", + "start_byte": 4992, + "end_byte": 5024 + }, + "old_position": 2, + "subtree": true, + "dest_start_byte": 4967, + "dest_end_byte": 4985, + "group_id": "group-1" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 27, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 6238, + "end_byte": 6240 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 27 + ], + "type": "if_statement", + "start_byte": 6238, + "end_byte": 6364 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 27, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 6252, + "end_byte": 6256 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 27 + ], + "type": "if_statement", + "start_byte": 6238, + "end_byte": 6364 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 27, + 3 + ], + "type": "comment", + "start_byte": 6261, + "end_byte": 6341 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 27 + ], + "type": "if_statement", + "start_byte": 6238, + "end_byte": 6364 + }, + "position": 4 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 27, + 4 + ], + "type": "block", + "start_byte": 6346, + "end_byte": 6358 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 27 + ], + "type": "if_statement", + "start_byte": 6238, + "end_byte": 6364 + }, + "position": 5, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 27, + 5 + ], + "type": "end", + "label": "end", + "start_byte": 6361, + "end_byte": 6364 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 27 + ], + "type": "if_statement", + "start_byte": 6238, + "end_byte": 6364 + }, + "position": 6 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 2, + 1, + 3 + ], + "type": "true", + "label": "true", + "start_byte": 4914, + "end_byte": 4918 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 2, + 1 + ], + "type": "arguments", + "start_byte": 4829, + "end_byte": 4949 + }, + "position": 3 + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 2, + 1, + 3 + ], + "type": "string", + "label": "'`opt.maxlen` must be \u003e= 8'", + "start_byte": 4961, + "end_byte": 4988 + }, + "old_value": "'`opt.maxlen` must be \u003e= 8'", + "new_value": "'`opts.maxlen` must be \u003e= 8'", + "dest_node": { + "tree": "after", + "path": [ + 123, + 3, + 2, + 1, + 4 + ], + "type": "string", + "label": "'`opts.maxlen` must be \u003e= 8'", + "start_byte": 4920, + "end_byte": 4948 + } + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 23, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 6113, + "end_byte": 6119 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 4958, + "end_byte": 4985 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 123, + 3, + 23, + 1 + ], + "type": "assignment_statement", + "start_byte": 6113, + "end_byte": 6133 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 4958, + "dest_end_byte": 4964 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 37, + 1, + 0 + ], + "type": "identifier", + "label": "t", + "start_byte": 7256, + "end_byte": 7257 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 37, + 1 + ], + "type": "binary_expression", + "start_byte": 7256, + "end_byte": 7271 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 37, + 1, + 1 + ], + "type": "comparison_operator_literal", + "label": "==", + "start_byte": 7258, + "end_byte": 7260 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 37, + 1 + ], + "type": "binary_expression", + "start_byte": 7256, + "end_byte": 7271 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 123, + 3, + 37, + 1, + 2 + ], + "type": "identifier", + "label": "tail_start", + "start_byte": 7261, + "end_byte": 7271 + }, + "parent": { + "tree": "after", + "path": [ + 123, + 3, + 37, + 1 + ], + "type": "binary_expression", + "start_byte": 7256, + "end_byte": 7271 + }, + "position": 2 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 2, + 1, + 2, + 2, + 0 + ], + "type": "if_statement", + "start_byte": 4869, + "end_byte": 4911 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 3, + 0 + ], + "type": "variable_list", + "start_byte": 4992, + "end_byte": 5003 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 23, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 6120, + "end_byte": 6121 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 23, + 1, + 2 + ], + "type": "expression_list", + "start_byte": 6122, + "end_byte": 6133 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 28, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 6306, + "end_byte": 6308 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 28, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 6320, + "end_byte": 6324 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 28, + 3 + ], + "type": "comment", + "start_byte": 6329, + "end_byte": 6384 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 28, + 4 + ], + "type": "block", + "start_byte": 6389, + "end_byte": 6416 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 28, + 5 + ], + "type": "end", + "label": "end", + "start_byte": 6419, + "end_byte": 6422 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 34 + ], + "type": "if_statement", + "start_byte": 7055, + "end_byte": 7102 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 38, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 7278, + "end_byte": 7293 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 38, + 1, + 1 + ], + "type": "logical_operator_literal", + "label": "and", + "start_byte": 7294, + "end_byte": 7297 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 123, + 3, + 38, + 1, + 2 + ], + "type": "binary_expression", + "start_byte": 7298, + "end_byte": 7305 + }, + "subtree": true + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": -1 + }, + { + "left_line": 181, + "right_line": -1 + }, + { + "left_line": 182, + "right_line": -1 + }, + { + "left_line": 183, + "right_line": 180 + }, + { + "left_line": 184, + "right_line": 181 + }, + { + "left_line": 185, + "right_line": 182 + }, + { + "left_line": 186, + "right_line": 183 + }, + { + "left_line": 187, + "right_line": 184 + }, + { + "left_line": 188, + "right_line": 185 + }, + { + "left_line": 189, + "right_line": 186 + }, + { + "left_line": 190, + "right_line": 187 + }, + { + "left_line": 191, + "right_line": 188 + }, + { + "left_line": 192, + "right_line": 189 + }, + { + "left_line": 193, + "right_line": 190 + }, + { + "left_line": 194, + "right_line": 191 + }, + { + "left_line": 195, + "right_line": 192 + }, + { + "left_line": 196, + "right_line": 193 + }, + { + "left_line": 197, + "right_line": 194 + }, + { + "left_line": 198, + "right_line": 195 + }, + { + "left_line": 199, + "right_line": 196 + }, + { + "left_line": 200, + "right_line": 197 + }, + { + "left_line": 201, + "right_line": 198 + }, + { + "left_line": 202, + "right_line": 199 + }, + { + "left_line": 203, + "right_line": 200 + }, + { + "left_line": 204, + "right_line": 201 + }, + { + "left_line": 205, + "right_line": 202 + }, + { + "left_line": 206, + "right_line": 203 + }, + { + "left_line": 207, + "right_line": 204 + }, + { + "left_line": 208, + "right_line": 205 + }, + { + "left_line": 209, + "right_line": 206 + }, + { + "left_line": 210, + "right_line": 207 + }, + { + "left_line": 211, + "right_line": 208 + }, + { + "left_line": 212, + "right_line": 209 + }, + { + "left_line": 213, + "right_line": 210 + }, + { + "left_line": 214, + "right_line": 211 + }, + { + "left_line": 215, + "right_line": 212 + }, + { + "left_line": 216, + "right_line": 213 + }, + { + "left_line": 217, + "right_line": 214 + }, + { + "left_line": 218, + "right_line": 215 + }, + { + "left_line": 219, + "right_line": 216 + }, + { + "left_line": 220, + "right_line": 217 + }, + { + "left_line": 221, + "right_line": 218 + }, + { + "left_line": 222, + "right_line": 219 + }, + { + "left_line": 223, + "right_line": -1 + }, + { + "left_line": 224, + "right_line": 220 + }, + { + "left_line": 225, + "right_line": 221 + }, + { + "left_line": 226, + "right_line": 222 + }, + { + "left_line": 227, + "right_line": 223 + }, + { + "left_line": 228, + "right_line": 224 + }, + { + "left_line": 229, + "right_line": 225 + }, + { + "left_line": 230, + "right_line": 226 + }, + { + "left_line": 231, + "right_line": 227 + }, + { + "left_line": 232, + "right_line": 228 + }, + { + "left_line": 233, + "right_line": 229 + }, + { + "left_line": 234, + "right_line": 230 + }, + { + "left_line": 235, + "right_line": 231 + }, + { + "left_line": 236, + "right_line": 232 + }, + { + "left_line": 237, + "right_line": 233 + }, + { + "left_line": 238, + "right_line": 234 + }, + { + "left_line": 239, + "right_line": 235 + }, + { + "left_line": 240, + "right_line": 236 + }, + { + "left_line": 241, + "right_line": 237 + }, + { + "left_line": 242, + "right_line": 238 + }, + { + "left_line": 243, + "right_line": 239 + }, + { + "left_line": 244, + "right_line": 240 + }, + { + "left_line": -1, + "right_line": 241 + }, + { + "left_line": 245, + "right_line": 242 + }, + { + "left_line": 246, + "right_line": -1 + }, + { + "left_line": 247, + "right_line": -1 + }, + { + "left_line": 248, + "right_line": -1 + }, + { + "left_line": 249, + "right_line": 243 + }, + { + "left_line": 250, + "right_line": 244 + }, + { + "left_line": 251, + "right_line": 245 + }, + { + "left_line": 252, + "right_line": 246 + }, + { + "left_line": 253, + "right_line": 247 + }, + { + "left_line": 254, + "right_line": 248 + }, + { + "left_line": 255, + "right_line": 249 + }, + { + "left_line": 256, + "right_line": 250 + }, + { + "left_line": 257, + "right_line": 251 + }, + { + "left_line": 258, + "right_line": 252 + }, + { + "left_line": 259, + "right_line": 253 + }, + { + "left_line": 260, + "right_line": 254 + }, + { + "left_line": 261, + "right_line": 255 + }, + { + "left_line": 262, + "right_line": 256 + }, + { + "left_line": 263, + "right_line": 257 + }, + { + "left_line": 264, + "right_line": 258 + }, + { + "left_line": 265, + "right_line": 259 + }, + { + "left_line": 266, + "right_line": 260 + }, + { + "left_line": 267, + "right_line": 261 + }, + { + "left_line": 268, + "right_line": 262 + }, + { + "left_line": 269, + "right_line": 263 + }, + { + "left_line": 270, + "right_line": 264 + }, + { + "left_line": 271, + "right_line": 265 + }, + { + "left_line": 272, + "right_line": 266 + }, + { + "left_line": 273, + "right_line": 267 + }, + { + "left_line": 274, + "right_line": 268 + }, + { + "left_line": 275, + "right_line": 269 + }, + { + "left_line": 276, + "right_line": 270 + }, + { + "left_line": 277, + "right_line": 271 + }, + { + "left_line": 278, + "right_line": 272 + }, + { + "left_line": 279, + "right_line": 273 + }, + { + "left_line": 280, + "right_line": 274 + }, + { + "left_line": 281, + "right_line": 275 + }, + { + "left_line": 282, + "right_line": 276 + }, + { + "left_line": 283, + "right_line": 277 + }, + { + "left_line": 284, + "right_line": 278 + }, + { + "left_line": 285, + "right_line": 279 + }, + { + "left_line": 286, + "right_line": 280 + }, + { + "left_line": 287, + "right_line": 281 + }, + { + "left_line": 288, + "right_line": 282 + }, + { + "left_line": 289, + "right_line": 283 + }, + { + "left_line": 290, + "right_line": 284 + }, + { + "left_line": 291, + "right_line": 285 + }, + { + "left_line": 292, + "right_line": 286 + }, + { + "left_line": 293, + "right_line": 287 + }, + { + "left_line": 294, + "right_line": 288 + }, + { + "left_line": 295, + "right_line": 289 + }, + { + "left_line": 296, + "right_line": 290 + }, + { + "left_line": 297, + "right_line": 291 + }, + { + "left_line": 298, + "right_line": 292 + }, + { + "left_line": 299, + "right_line": 293 + }, + { + "left_line": 300, + "right_line": 294 + }, + { + "left_line": 301, + "right_line": 295 + }, + { + "left_line": 302, + "right_line": 296 + }, + { + "left_line": 303, + "right_line": 297 + }, + { + "left_line": 304, + "right_line": 298 + }, + { + "left_line": 305, + "right_line": 299 + }, + { + "left_line": 306, + "right_line": 300 + }, + { + "left_line": 307, + "right_line": 301 + }, + { + "left_line": 308, + "right_line": 302 + }, + { + "left_line": 309, + "right_line": 303 + }, + { + "left_line": 310, + "right_line": 304 + }, + { + "left_line": 311, + "right_line": 305 + }, + { + "left_line": 312, + "right_line": 306 + }, + { + "left_line": 313, + "right_line": 307 + }, + { + "left_line": 314, + "right_line": 308 + }, + { + "left_line": 315, + "right_line": 309 + }, + { + "left_line": 316, + "right_line": 310 + }, + { + "left_line": 317, + "right_line": 311 + }, + { + "left_line": 318, + "right_line": 312 + }, + { + "left_line": 319, + "right_line": 313 + }, + { + "left_line": 320, + "right_line": 314 + }, + { + "left_line": 321, + "right_line": 315 + }, + { + "left_line": 322, + "right_line": 316 + }, + { + "left_line": 323, + "right_line": 317 + }, + { + "left_line": 324, + "right_line": 318 + }, + { + "left_line": 325, + "right_line": 319 + }, + { + "left_line": 326, + "right_line": 320 + }, + { + "left_line": 327, + "right_line": 321 + }, + { + "left_line": 328, + "right_line": 322 + }, + { + "left_line": 329, + "right_line": 323 + }, + { + "left_line": 330, + "right_line": 324 + }, + { + "left_line": 331, + "right_line": 325 + }, + { + "left_line": 332, + "right_line": 326 + }, + { + "left_line": 333, + "right_line": 327 + }, + { + "left_line": 334, + "right_line": 328 + }, + { + "left_line": 335, + "right_line": 329 + }, + { + "left_line": 336, + "right_line": 330 + }, + { + "left_line": 337, + "right_line": 331 + }, + { + "left_line": 338, + "right_line": 332 + }, + { + "left_line": 339, + "right_line": 333 + }, + { + "left_line": 340, + "right_line": 334 + }, + { + "left_line": 341, + "right_line": 335 + }, + { + "left_line": 342, + "right_line": 336 + }, + { + "left_line": 343, + "right_line": 337 + }, + { + "left_line": 344, + "right_line": 338 + }, + { + "left_line": 345, + "right_line": 339 + }, + { + "left_line": 346, + "right_line": 340 + }, + { + "left_line": 347, + "right_line": 341 + }, + { + "left_line": 348, + "right_line": 342 + }, + { + "left_line": 349, + "right_line": 343 + }, + { + "left_line": 350, + "right_line": 344 + }, + { + "left_line": 351, + "right_line": 345 + }, + { + "left_line": 352, + "right_line": 346 + }, + { + "left_line": 353, + "right_line": 347 + }, + { + "left_line": 354, + "right_line": 348 + }, + { + "left_line": 355, + "right_line": 349 + }, + { + "left_line": 356, + "right_line": 350 + }, + { + "left_line": 357, + "right_line": 351 + }, + { + "left_line": 358, + "right_line": 352 + }, + { + "left_line": 359, + "right_line": 353 + }, + { + "left_line": 360, + "right_line": 354 + }, + { + "left_line": 361, + "right_line": 355 + }, + { + "left_line": 362, + "right_line": 356 + }, + { + "left_line": 363, + "right_line": 357 + }, + { + "left_line": 364, + "right_line": 358 + }, + { + "left_line": 365, + "right_line": 359 + }, + { + "left_line": 366, + "right_line": 360 + }, + { + "left_line": 367, + "right_line": 361 + }, + { + "left_line": 368, + "right_line": 362 + }, + { + "left_line": 369, + "right_line": 363 + }, + { + "left_line": 370, + "right_line": 364 + }, + { + "left_line": 371, + "right_line": 365 + }, + { + "left_line": 372, + "right_line": 366 + }, + { + "left_line": 373, + "right_line": 367 + }, + { + "left_line": 374, + "right_line": 368 + }, + { + "left_line": 375, + "right_line": 369 + }, + { + "left_line": 376, + "right_line": 370 + }, + { + "left_line": 377, + "right_line": 371 + }, + { + "left_line": 378, + "right_line": 372 + }, + { + "left_line": 379, + "right_line": 373 + }, + { + "left_line": 380, + "right_line": 374 + }, + { + "left_line": 381, + "right_line": 375 + }, + { + "left_line": 382, + "right_line": 376 + }, + { + "left_line": 383, + "right_line": 377 + }, + { + "left_line": 384, + "right_line": 378 + }, + { + "left_line": 385, + "right_line": 379 + }, + { + "left_line": 386, + "right_line": 380 + }, + { + "left_line": 387, + "right_line": 381 + }, + { + "left_line": 388, + "right_line": 382 + }, + { + "left_line": 389, + "right_line": 383 + }, + { + "left_line": 390, + "right_line": 384 + }, + { + "left_line": 391, + "right_line": 385 + }, + { + "left_line": 392, + "right_line": 386 + }, + { + "left_line": 393, + "right_line": 387 + }, + { + "left_line": 394, + "right_line": 388 + }, + { + "left_line": 395, + "right_line": 389 + }, + { + "left_line": 396, + "right_line": 390 + }, + { + "left_line": 397, + "right_line": 391 + }, + { + "left_line": 398, + "right_line": 392 + }, + { + "left_line": 399, + "right_line": 393 + }, + { + "left_line": 400, + "right_line": 394 + }, + { + "left_line": 401, + "right_line": 395 + }, + { + "left_line": 402, + "right_line": 396 + }, + { + "left_line": 403, + "right_line": 397 + }, + { + "left_line": 404, + "right_line": 398 + }, + { + "left_line": 405, + "right_line": 399 + }, + { + "left_line": 406, + "right_line": 400 + }, + { + "left_line": 407, + "right_line": 401 + }, + { + "left_line": 408, + "right_line": 402 + }, + { + "left_line": 409, + "right_line": 403 + }, + { + "left_line": 410, + "right_line": 404 + }, + { + "left_line": 411, + "right_line": 405 + }, + { + "left_line": 412, + "right_line": 406 + }, + { + "left_line": 413, + "right_line": 407 + }, + { + "left_line": 414, + "right_line": 408 + }, + { + "left_line": 415, + "right_line": 409 + }, + { + "left_line": 416, + "right_line": 410 + }, + { + "left_line": 417, + "right_line": 411 + }, + { + "left_line": 418, + "right_line": 412 + }, + { + "left_line": 419, + "right_line": 413 + }, + { + "left_line": 420, + "right_line": 414 + }, + { + "left_line": 421, + "right_line": 415 + }, + { + "left_line": 422, + "right_line": 416 + }, + { + "left_line": 423, + "right_line": 417 + }, + { + "left_line": 424, + "right_line": 418 + }, + { + "left_line": 425, + "right_line": 419 + }, + { + "left_line": 426, + "right_line": 420 + }, + { + "left_line": 427, + "right_line": 421 + }, + { + "left_line": 428, + "right_line": 422 + }, + { + "left_line": 429, + "right_line": 423 + }, + { + "left_line": 430, + "right_line": 424 + }, + { + "left_line": 431, + "right_line": 425 + }, + { + "left_line": 432, + "right_line": 426 + }, + { + "left_line": 433, + "right_line": 427 + }, + { + "left_line": 434, + "right_line": 428 + }, + { + "left_line": 435, + "right_line": 429 + }, + { + "left_line": 436, + "right_line": 430 + }, + { + "left_line": 437, + "right_line": 431 + }, + { + "left_line": 438, + "right_line": 432 + }, + { + "left_line": 439, + "right_line": 433 + }, + { + "left_line": 440, + "right_line": 434 + }, + { + "left_line": 441, + "right_line": 435 + }, + { + "left_line": 442, + "right_line": 436 + }, + { + "left_line": 443, + "right_line": 437 + }, + { + "left_line": 444, + "right_line": 438 + }, + { + "left_line": 445, + "right_line": 439 + }, + { + "left_line": 446, + "right_line": 440 + }, + { + "left_line": 447, + "right_line": 441 + }, + { + "left_line": 448, + "right_line": 442 + }, + { + "left_line": 449, + "right_line": 443 + }, + { + "left_line": 450, + "right_line": 444 + }, + { + "left_line": 451, + "right_line": 445 + }, + { + "left_line": 452, + "right_line": 446 + }, + { + "left_line": 453, + "right_line": 447 + }, + { + "left_line": 454, + "right_line": 448 + }, + { + "left_line": 455, + "right_line": 449 + }, + { + "left_line": 456, + "right_line": 450 + }, + { + "left_line": 457, + "right_line": 451 + }, + { + "left_line": 458, + "right_line": 452 + }, + { + "left_line": 459, + "right_line": 453 + }, + { + "left_line": 460, + "right_line": 454 + }, + { + "left_line": 461, + "right_line": 455 + }, + { + "left_line": 462, + "right_line": 456 + }, + { + "left_line": 463, + "right_line": 457 + }, + { + "left_line": 464, + "right_line": 458 + }, + { + "left_line": 465, + "right_line": 459 + }, + { + "left_line": 466, + "right_line": 460 + }, + { + "left_line": 467, + "right_line": 461 + }, + { + "left_line": 468, + "right_line": 462 + }, + { + "left_line": 469, + "right_line": 463 + }, + { + "left_line": 470, + "right_line": 464 + }, + { + "left_line": 471, + "right_line": 465 + }, + { + "left_line": 472, + "right_line": 466 + }, + { + "left_line": 473, + "right_line": 467 + }, + { + "left_line": 474, + "right_line": 468 + }, + { + "left_line": 475, + "right_line": 469 + }, + { + "left_line": 476, + "right_line": 470 + }, + { + "left_line": 477, + "right_line": 471 + }, + { + "left_line": 478, + "right_line": 472 + }, + { + "left_line": 479, + "right_line": 473 + }, + { + "left_line": 480, + "right_line": 474 + }, + { + "left_line": 481, + "right_line": 475 + }, + { + "left_line": 482, + "right_line": 476 + }, + { + "left_line": 483, + "right_line": 477 + }, + { + "left_line": 484, + "right_line": 478 + }, + { + "left_line": 485, + "right_line": 479 + }, + { + "left_line": 486, + "right_line": 480 + }, + { + "left_line": 487, + "right_line": 481 + }, + { + "left_line": 488, + "right_line": 482 + }, + { + "left_line": 489, + "right_line": 483 + }, + { + "left_line": 490, + "right_line": 484 + }, + { + "left_line": 491, + "right_line": 485 + }, + { + "left_line": 492, + "right_line": 486 + }, + { + "left_line": 493, + "right_line": 487 + }, + { + "left_line": 494, + "right_line": 488 + }, + { + "left_line": 495, + "right_line": 489 + }, + { + "left_line": 496, + "right_line": 490 + }, + { + "left_line": 497, + "right_line": 491 + }, + { + "left_line": 498, + "right_line": 492 + }, + { + "left_line": 499, + "right_line": 493 + }, + { + "left_line": 500, + "right_line": 494 + }, + { + "left_line": 501, + "right_line": 495 + }, + { + "left_line": 502, + "right_line": 496 + }, + { + "left_line": 503, + "right_line": 497 + }, + { + "left_line": 504, + "right_line": 498 + }, + { + "left_line": 505, + "right_line": 499 + }, + { + "left_line": 506, + "right_line": 500 + }, + { + "left_line": 507, + "right_line": 501 + }, + { + "left_line": 508, + "right_line": 502 + }, + { + "left_line": 509, + "right_line": 503 + }, + { + "left_line": 510, + "right_line": 504 + }, + { + "left_line": 511, + "right_line": 505 + }, + { + "left_line": 512, + "right_line": 506 + }, + { + "left_line": 513, + "right_line": 507 + }, + { + "left_line": 514, + "right_line": 508 + }, + { + "left_line": 515, + "right_line": 509 + }, + { + "left_line": 516, + "right_line": 510 + }, + { + "left_line": 517, + "right_line": 511 + }, + { + "left_line": 518, + "right_line": 512 + }, + { + "left_line": 519, + "right_line": 513 + }, + { + "left_line": 520, + "right_line": 514 + }, + { + "left_line": 521, + "right_line": 515 + }, + { + "left_line": 522, + "right_line": 516 + }, + { + "left_line": 523, + "right_line": 517 + }, + { + "left_line": 524, + "right_line": 518 + }, + { + "left_line": 525, + "right_line": 519 + }, + { + "left_line": 526, + "right_line": 520 + }, + { + "left_line": 527, + "right_line": 521 + }, + { + "left_line": 528, + "right_line": 522 + }, + { + "left_line": 529, + "right_line": 523 + }, + { + "left_line": 530, + "right_line": 524 + }, + { + "left_line": 531, + "right_line": 525 + }, + { + "left_line": 532, + "right_line": 526 + }, + { + "left_line": 533, + "right_line": 527 + }, + { + "left_line": 534, + "right_line": 528 + }, + { + "left_line": 535, + "right_line": 529 + }, + { + "left_line": 536, + "right_line": 530 + }, + { + "left_line": 537, + "right_line": 531 + }, + { + "left_line": 538, + "right_line": 532 + }, + { + "left_line": 539, + "right_line": 533 + }, + { + "left_line": 540, + "right_line": 534 + }, + { + "left_line": 541, + "right_line": 535 + }, + { + "left_line": 542, + "right_line": 536 + }, + { + "left_line": 543, + "right_line": 537 + }, + { + "left_line": 544, + "right_line": 538 + }, + { + "left_line": 545, + "right_line": 539 + }, + { + "left_line": 546, + "right_line": 540 + }, + { + "left_line": 547, + "right_line": 541 + }, + { + "left_line": 548, + "right_line": 542 + }, + { + "left_line": 549, + "right_line": 543 + }, + { + "left_line": 550, + "right_line": 544 + }, + { + "left_line": 551, + "right_line": 545 + }, + { + "left_line": 552, + "right_line": 546 + }, + { + "left_line": 553, + "right_line": 547 + }, + { + "left_line": 554, + "right_line": 548 + }, + { + "left_line": 555, + "right_line": 549 + }, + { + "left_line": 556, + "right_line": 550 + }, + { + "left_line": 557, + "right_line": 551 + }, + { + "left_line": 558, + "right_line": 552 + }, + { + "left_line": 559, + "right_line": 553 + }, + { + "left_line": 560, + "right_line": 554 + }, + { + "left_line": 561, + "right_line": 555 + }, + { + "left_line": 562, + "right_line": 556 + }, + { + "left_line": 563, + "right_line": 557 + }, + { + "left_line": 564, + "right_line": 558 + }, + { + "left_line": 565, + "right_line": 559 + }, + { + "left_line": 566, + "right_line": 560 + }, + { + "left_line": 567, + "right_line": 561 + }, + { + "left_line": 568, + "right_line": 562 + }, + { + "left_line": 569, + "right_line": 563 + }, + { + "left_line": 570, + "right_line": 564 + }, + { + "left_line": 571, + "right_line": 565 + }, + { + "left_line": 572, + "right_line": 566 + }, + { + "left_line": 573, + "right_line": 567 + }, + { + "left_line": 574, + "right_line": 568 + }, + { + "left_line": 575, + "right_line": 569 + }, + { + "left_line": 576, + "right_line": 570 + }, + { + "left_line": 577, + "right_line": 571 + }, + { + "left_line": 578, + "right_line": 572 + }, + { + "left_line": 579, + "right_line": 573 + }, + { + "left_line": 580, + "right_line": 574 + }, + { + "left_line": 581, + "right_line": 575 + }, + { + "left_line": 582, + "right_line": 576 + }, + { + "left_line": 583, + "right_line": 577 + }, + { + "left_line": 584, + "right_line": 578 + }, + { + "left_line": 585, + "right_line": 579 + }, + { + "left_line": 586, + "right_line": 580 + }, + { + "left_line": 587, + "right_line": 581 + }, + { + "left_line": 588, + "right_line": 582 + }, + { + "left_line": 589, + "right_line": 583 + }, + { + "left_line": 590, + "right_line": 584 + }, + { + "left_line": 591, + "right_line": 585 + }, + { + "left_line": 592, + "right_line": 586 + }, + { + "left_line": 593, + "right_line": 587 + }, + { + "left_line": 594, + "right_line": 588 + }, + { + "left_line": 595, + "right_line": 589 + }, + { + "left_line": 596, + "right_line": 590 + }, + { + "left_line": 597, + "right_line": 591 + }, + { + "left_line": 598, + "right_line": 592 + }, + { + "left_line": 599, + "right_line": 593 + }, + { + "left_line": 600, + "right_line": 594 + }, + { + "left_line": 601, + "right_line": 595 + }, + { + "left_line": 602, + "right_line": 596 + }, + { + "left_line": 603, + "right_line": 597 + }, + { + "left_line": 604, + "right_line": 598 + }, + { + "left_line": 605, + "right_line": 599 + }, + { + "left_line": 606, + "right_line": 600 + }, + { + "left_line": 607, + "right_line": 601 + }, + { + "left_line": 608, + "right_line": 602 + }, + { + "left_line": 609, + "right_line": 603 + }, + { + "left_line": 610, + "right_line": 604 + }, + { + "left_line": 611, + "right_line": 605 + }, + { + "left_line": 612, + "right_line": 606 + }, + { + "left_line": 613, + "right_line": 607 + }, + { + "left_line": 614, + "right_line": 608 + }, + { + "left_line": 615, + "right_line": 609 + }, + { + "left_line": 616, + "right_line": 610 + }, + { + "left_line": 617, + "right_line": 611 + }, + { + "left_line": 618, + "right_line": 612 + }, + { + "left_line": 619, + "right_line": 613 + }, + { + "left_line": 620, + "right_line": 614 + }, + { + "left_line": 621, + "right_line": 615 + }, + { + "left_line": 622, + "right_line": 616 + }, + { + "left_line": 623, + "right_line": 617 + }, + { + "left_line": 624, + "right_line": 618 + }, + { + "left_line": 625, + "right_line": 619 + }, + { + "left_line": 626, + "right_line": 620 + }, + { + "left_line": 627, + "right_line": 621 + }, + { + "left_line": 628, + "right_line": 622 + }, + { + "left_line": 629, + "right_line": 623 + }, + { + "left_line": 630, + "right_line": 624 + }, + { + "left_line": 631, + "right_line": 625 + }, + { + "left_line": 632, + "right_line": 626 + }, + { + "left_line": 633, + "right_line": 627 + }, + { + "left_line": 634, + "right_line": 628 + }, + { + "left_line": 635, + "right_line": 629 + }, + { + "left_line": 636, + "right_line": 630 + }, + { + "left_line": 637, + "right_line": 631 + }, + { + "left_line": 638, + "right_line": 632 + }, + { + "left_line": 639, + "right_line": 633 + }, + { + "left_line": 640, + "right_line": 634 + }, + { + "left_line": 641, + "right_line": 635 + }, + { + "left_line": 642, + "right_line": 636 + }, + { + "left_line": 643, + "right_line": 637 + }, + { + "left_line": 644, + "right_line": 638 + }, + { + "left_line": 645, + "right_line": 639 + }, + { + "left_line": 646, + "right_line": 640 + }, + { + "left_line": 647, + "right_line": 641 + }, + { + "left_line": 648, + "right_line": 642 + }, + { + "left_line": 649, + "right_line": 643 + }, + { + "left_line": 650, + "right_line": 644 + }, + { + "left_line": 651, + "right_line": 645 + }, + { + "left_line": 652, + "right_line": 646 + }, + { + "left_line": 653, + "right_line": 647 + }, + { + "left_line": 654, + "right_line": 648 + }, + { + "left_line": 655, + "right_line": 649 + }, + { + "left_line": 656, + "right_line": 650 + }, + { + "left_line": 657, + "right_line": 651 + }, + { + "left_line": 658, + "right_line": 652 + }, + { + "left_line": 659, + "right_line": 653 + }, + { + "left_line": 660, + "right_line": 654 + }, + { + "left_line": 661, + "right_line": 655 + }, + { + "left_line": 662, + "right_line": 656 + }, + { + "left_line": 663, + "right_line": 657 + }, + { + "left_line": 664, + "right_line": 658 + }, + { + "left_line": 665, + "right_line": 659 + }, + { + "left_line": 666, + "right_line": 660 + }, + { + "left_line": 667, + "right_line": 661 + }, + { + "left_line": 668, + "right_line": 662 + }, + { + "left_line": 669, + "right_line": 663 + }, + { + "left_line": 670, + "right_line": 664 + }, + { + "left_line": 671, + "right_line": 665 + }, + { + "left_line": 672, + "right_line": 666 + }, + { + "left_line": 673, + "right_line": 667 + }, + { + "left_line": 674, + "right_line": 668 + }, + { + "left_line": 675, + "right_line": 669 + }, + { + "left_line": 676, + "right_line": 670 + }, + { + "left_line": 677, + "right_line": 671 + }, + { + "left_line": 678, + "right_line": 672 + }, + { + "left_line": 679, + "right_line": 673 + }, + { + "left_line": 680, + "right_line": 674 + }, + { + "left_line": 681, + "right_line": 675 + }, + { + "left_line": 682, + "right_line": 676 + }, + { + "left_line": 683, + "right_line": 677 + }, + { + "left_line": 684, + "right_line": 678 + }, + { + "left_line": 685, + "right_line": 679 + }, + { + "left_line": 686, + "right_line": 680 + }, + { + "left_line": 687, + "right_line": 681 + }, + { + "left_line": 688, + "right_line": 682 + }, + { + "left_line": 689, + "right_line": 683 + }, + { + "left_line": 690, + "right_line": 684 + }, + { + "left_line": 691, + "right_line": 685 + }, + { + "left_line": 692, + "right_line": 686 + }, + { + "left_line": 693, + "right_line": 687 + }, + { + "left_line": 694, + "right_line": 688 + }, + { + "left_line": 695, + "right_line": 689 + }, + { + "left_line": 696, + "right_line": 690 + }, + { + "left_line": 697, + "right_line": 691 + }, + { + "left_line": 698, + "right_line": 692 + }, + { + "left_line": 699, + "right_line": 693 + }, + { + "left_line": 700, + "right_line": 694 + }, + { + "left_line": 701, + "right_line": 695 + }, + { + "left_line": 702, + "right_line": 696 + }, + { + "left_line": 703, + "right_line": 697 + }, + { + "left_line": 704, + "right_line": 698 + }, + { + "left_line": 705, + "right_line": 699 + }, + { + "left_line": 706, + "right_line": 700 + }, + { + "left_line": 707, + "right_line": 701 + }, + { + "left_line": 708, + "right_line": 702 + }, + { + "left_line": 709, + "right_line": 703 + }, + { + "left_line": 710, + "right_line": 704 + }, + { + "left_line": 711, + "right_line": 705 + }, + { + "left_line": 712, + "right_line": 706 + }, + { + "left_line": 713, + "right_line": 707 + }, + { + "left_line": 714, + "right_line": 708 + }, + { + "left_line": 715, + "right_line": 709 + }, + { + "left_line": 716, + "right_line": 710 + }, + { + "left_line": 717, + "right_line": 711 + }, + { + "left_line": 718, + "right_line": 712 + }, + { + "left_line": 719, + "right_line": 713 + }, + { + "left_line": 720, + "right_line": 714 + }, + { + "left_line": 721, + "right_line": 715 + }, + { + "left_line": 722, + "right_line": 716 + }, + { + "left_line": 723, + "right_line": 717 + }, + { + "left_line": 724, + "right_line": 718 + }, + { + "left_line": 725, + "right_line": 719 + }, + { + "left_line": 726, + "right_line": 720 + }, + { + "left_line": 727, + "right_line": 721 + }, + { + "left_line": 728, + "right_line": 722 + }, + { + "left_line": 729, + "right_line": 723 + }, + { + "left_line": 730, + "right_line": 724 + }, + { + "left_line": 731, + "right_line": 725 + }, + { + "left_line": 732, + "right_line": 726 + }, + { + "left_line": 733, + "right_line": 727 + }, + { + "left_line": 734, + "right_line": 728 + }, + { + "left_line": 735, + "right_line": 729 + }, + { + "left_line": 736, + "right_line": 730 + }, + { + "left_line": 737, + "right_line": 731 + }, + { + "left_line": 738, + "right_line": 732 + }, + { + "left_line": 739, + "right_line": 733 + }, + { + "left_line": 740, + "right_line": 734 + }, + { + "left_line": 741, + "right_line": 735 + }, + { + "left_line": 742, + "right_line": 736 + }, + { + "left_line": 743, + "right_line": 737 + }, + { + "left_line": 744, + "right_line": 738 + }, + { + "left_line": 745, + "right_line": 739 + }, + { + "left_line": 746, + "right_line": 740 + }, + { + "left_line": 747, + "right_line": 741 + }, + { + "left_line": 748, + "right_line": 742 + }, + { + "left_line": 749, + "right_line": 743 + }, + { + "left_line": 750, + "right_line": 744 + }, + { + "left_line": 751, + "right_line": 745 + }, + { + "left_line": 752, + "right_line": 746 + }, + { + "left_line": 753, + "right_line": 747 + }, + { + "left_line": 754, + "right_line": 748 + }, + { + "left_line": 755, + "right_line": 749 + }, + { + "left_line": 756, + "right_line": 750 + }, + { + "left_line": 757, + "right_line": 751 + }, + { + "left_line": 758, + "right_line": 752 + }, + { + "left_line": 759, + "right_line": 753 + }, + { + "left_line": 760, + "right_line": 754 + }, + { + "left_line": 761, + "right_line": 755 + }, + { + "left_line": 762, + "right_line": 756 + }, + { + "left_line": 763, + "right_line": 757 + }, + { + "left_line": 764, + "right_line": 758 + }, + { + "left_line": 765, + "right_line": 759 + }, + { + "left_line": 766, + "right_line": 760 + }, + { + "left_line": 767, + "right_line": 761 + }, + { + "left_line": 768, + "right_line": 762 + }, + { + "left_line": 769, + "right_line": 763 + }, + { + "left_line": 770, + "right_line": 764 + }, + { + "left_line": 771, + "right_line": 765 + }, + { + "left_line": 772, + "right_line": 766 + }, + { + "left_line": 773, + "right_line": 767 + }, + { + "left_line": 774, + "right_line": 768 + }, + { + "left_line": 775, + "right_line": 769 + }, + { + "left_line": 776, + "right_line": 770 + }, + { + "left_line": 777, + "right_line": 771 + }, + { + "left_line": 778, + "right_line": 772 + }, + { + "left_line": 779, + "right_line": 773 + }, + { + "left_line": 780, + "right_line": 774 + }, + { + "left_line": 781, + "right_line": 775 + }, + { + "left_line": 782, + "right_line": 776 + }, + { + "left_line": 783, + "right_line": 777 + }, + { + "left_line": 784, + "right_line": 778 + }, + { + "left_line": 785, + "right_line": 779 + }, + { + "left_line": 786, + "right_line": 780 + }, + { + "left_line": 787, + "right_line": 781 + }, + { + "left_line": 788, + "right_line": 782 + }, + { + "left_line": 789, + "right_line": 783 + }, + { + "left_line": 790, + "right_line": 784 + }, + { + "left_line": 791, + "right_line": 785 + }, + { + "left_line": 792, + "right_line": 786 + }, + { + "left_line": 793, + "right_line": 787 + }, + { + "left_line": 794, + "right_line": 788 + }, + { + "left_line": 795, + "right_line": 789 + }, + { + "left_line": 796, + "right_line": 790 + }, + { + "left_line": 797, + "right_line": 791 + }, + { + "left_line": 798, + "right_line": 792 + }, + { + "left_line": 799, + "right_line": 793 + }, + { + "left_line": 800, + "right_line": 794 + }, + { + "left_line": 801, + "right_line": 795 + }, + { + "left_line": 802, + "right_line": 796 + }, + { + "left_line": 803, + "right_line": 797 + }, + { + "left_line": 804, + "right_line": 798 + }, + { + "left_line": 805, + "right_line": 799 + }, + { + "left_line": 806, + "right_line": 800 + }, + { + "left_line": 807, + "right_line": 801 + }, + { + "left_line": 808, + "right_line": 802 + }, + { + "left_line": 809, + "right_line": 803 + }, + { + "left_line": 810, + "right_line": 804 + }, + { + "left_line": 811, + "right_line": 805 + }, + { + "left_line": 812, + "right_line": 806 + }, + { + "left_line": 813, + "right_line": 807 + }, + { + "left_line": 814, + "right_line": 808 + }, + { + "left_line": 815, + "right_line": 809 + }, + { + "left_line": 816, + "right_line": 810 + }, + { + "left_line": 817, + "right_line": 811 + }, + { + "left_line": 818, + "right_line": 812 + }, + { + "left_line": 819, + "right_line": 813 + }, + { + "left_line": 820, + "right_line": 814 + }, + { + "left_line": 821, + "right_line": 815 + }, + { + "left_line": 822, + "right_line": 816 + }, + { + "left_line": 823, + "right_line": 817 + }, + { + "left_line": 824, + "right_line": 818 + }, + { + "left_line": 825, + "right_line": 819 + }, + { + "left_line": 826, + "right_line": 820 + }, + { + "left_line": 827, + "right_line": 821 + }, + { + "left_line": 828, + "right_line": 822 + }, + { + "left_line": 829, + "right_line": 823 + }, + { + "left_line": 830, + "right_line": 824 + }, + { + "left_line": 831, + "right_line": 825 + }, + { + "left_line": 832, + "right_line": 826 + }, + { + "left_line": 833, + "right_line": 827 + }, + { + "left_line": 834, + "right_line": 828 + }, + { + "left_line": 835, + "right_line": 829 + }, + { + "left_line": 836, + "right_line": 830 + }, + { + "left_line": 837, + "right_line": 831 + }, + { + "left_line": 838, + "right_line": 832 + }, + { + "left_line": 839, + "right_line": 833 + }, + { + "left_line": 840, + "right_line": 834 + }, + { + "left_line": 841, + "right_line": 835 + }, + { + "left_line": 842, + "right_line": 836 + }, + { + "left_line": 843, + "right_line": 837 + }, + { + "left_line": 844, + "right_line": 838 + }, + { + "left_line": 845, + "right_line": 839 + }, + { + "left_line": 846, + "right_line": 840 + }, + { + "left_line": 847, + "right_line": 841 + }, + { + "left_line": 848, + "right_line": 842 + }, + { + "left_line": 849, + "right_line": 843 + }, + { + "left_line": 850, + "right_line": 844 + }, + { + "left_line": 851, + "right_line": 845 + }, + { + "left_line": 852, + "right_line": 846 + }, + { + "left_line": 853, + "right_line": 847 + }, + { + "left_line": 854, + "right_line": 848 + }, + { + "left_line": 855, + "right_line": 849 + }, + { + "left_line": 856, + "right_line": 850 + }, + { + "left_line": 857, + "right_line": 851 + }, + { + "left_line": 858, + "right_line": 852 + }, + { + "left_line": 859, + "right_line": 853 + }, + { + "left_line": 860, + "right_line": 854 + }, + { + "left_line": 861, + "right_line": 855 + }, + { + "left_line": 862, + "right_line": 856 + }, + { + "left_line": 863, + "right_line": 857 + }, + { + "left_line": 864, + "right_line": 858 + }, + { + "left_line": 865, + "right_line": 859 + }, + { + "left_line": 866, + "right_line": 860 + }, + { + "left_line": 867, + "right_line": 861 + }, + { + "left_line": 868, + "right_line": 862 + }, + { + "left_line": 869, + "right_line": 863 + }, + { + "left_line": 870, + "right_line": 864 + }, + { + "left_line": 871, + "right_line": 865 + }, + { + "left_line": 872, + "right_line": 866 + }, + { + "left_line": 873, + "right_line": 867 + }, + { + "left_line": 874, + "right_line": 868 + }, + { + "left_line": 875, + "right_line": 869 + }, + { + "left_line": 876, + "right_line": 870 + }, + { + "left_line": 877, + "right_line": 871 + }, + { + "left_line": 878, + "right_line": 872 + }, + { + "left_line": 879, + "right_line": 873 + }, + { + "left_line": 880, + "right_line": 874 + }, + { + "left_line": 881, + "right_line": 875 + }, + { + "left_line": 882, + "right_line": 876 + }, + { + "left_line": 883, + "right_line": 877 + }, + { + "left_line": 884, + "right_line": 878 + }, + { + "left_line": 885, + "right_line": 879 + }, + { + "left_line": 886, + "right_line": 880 + }, + { + "left_line": 887, + "right_line": 881 + }, + { + "left_line": 888, + "right_line": 882 + }, + { + "left_line": 889, + "right_line": 883 + }, + { + "left_line": 890, + "right_line": 884 + }, + { + "left_line": 891, + "right_line": 885 + }, + { + "left_line": 892, + "right_line": 886 + }, + { + "left_line": 893, + "right_line": 887 + }, + { + "left_line": 894, + "right_line": 888 + }, + { + "left_line": 895, + "right_line": 889 + }, + { + "left_line": 896, + "right_line": 890 + }, + { + "left_line": 897, + "right_line": 891 + }, + { + "left_line": 898, + "right_line": 892 + }, + { + "left_line": 899, + "right_line": 893 + }, + { + "left_line": 900, + "right_line": 894 + }, + { + "left_line": 901, + "right_line": 895 + }, + { + "left_line": 902, + "right_line": 896 + }, + { + "left_line": 903, + "right_line": 897 + }, + { + "left_line": 904, + "right_line": 898 + }, + { + "left_line": 905, + "right_line": 899 + }, + { + "left_line": 906, + "right_line": 900 + }, + { + "left_line": 907, + "right_line": 901 + }, + { + "left_line": 908, + "right_line": 902 + }, + { + "left_line": 909, + "right_line": 903 + }, + { + "left_line": 910, + "right_line": 904 + }, + { + "left_line": 911, + "right_line": 905 + }, + { + "left_line": 912, + "right_line": 906 + }, + { + "left_line": 913, + "right_line": 907 + }, + { + "left_line": 914, + "right_line": 908 + }, + { + "left_line": 915, + "right_line": 909 + }, + { + "left_line": 916, + "right_line": 910 + }, + { + "left_line": 917, + "right_line": 911 + }, + { + "left_line": 918, + "right_line": 912 + }, + { + "left_line": 919, + "right_line": 913 + }, + { + "left_line": 920, + "right_line": 914 + }, + { + "left_line": 921, + "right_line": 915 + }, + { + "left_line": 922, + "right_line": 916 + }, + { + "left_line": 923, + "right_line": 917 + }, + { + "left_line": 924, + "right_line": 918 + }, + { + "left_line": 925, + "right_line": 919 + }, + { + "left_line": 926, + "right_line": 920 + }, + { + "left_line": 927, + "right_line": 921 + }, + { + "left_line": 928, + "right_line": 922 + }, + { + "left_line": 929, + "right_line": 923 + }, + { + "left_line": 930, + "right_line": 924 + }, + { + "left_line": 931, + "right_line": 925 + }, + { + "left_line": 932, + "right_line": 926 + }, + { + "left_line": 933, + "right_line": 927 + }, + { + "left_line": 934, + "right_line": 928 + }, + { + "left_line": 935, + "right_line": 929 + }, + { + "left_line": 936, + "right_line": 930 + }, + { + "left_line": 937, + "right_line": 931 + }, + { + "left_line": 938, + "right_line": 932 + }, + { + "left_line": 939, + "right_line": 933 + }, + { + "left_line": 940, + "right_line": 934 + }, + { + "left_line": 941, + "right_line": 935 + }, + { + "left_line": 942, + "right_line": 936 + }, + { + "left_line": 943, + "right_line": 937 + }, + { + "left_line": 944, + "right_line": 938 + }, + { + "left_line": 945, + "right_line": 939 + }, + { + "left_line": 946, + "right_line": 940 + }, + { + "left_line": 947, + "right_line": 941 + }, + { + "left_line": 948, + "right_line": 942 + }, + { + "left_line": 949, + "right_line": 943 + }, + { + "left_line": 950, + "right_line": 944 + }, + { + "left_line": 951, + "right_line": 945 + }, + { + "left_line": 952, + "right_line": 946 + }, + { + "left_line": 953, + "right_line": 947 + }, + { + "left_line": 954, + "right_line": 948 + }, + { + "left_line": 955, + "right_line": 949 + }, + { + "left_line": 956, + "right_line": 950 + }, + { + "left_line": 957, + "right_line": 951 + }, + { + "left_line": 958, + "right_line": 952 + }, + { + "left_line": 959, + "right_line": 953 + }, + { + "left_line": 960, + "right_line": 954 + }, + { + "left_line": 961, + "right_line": 955 + }, + { + "left_line": 962, + "right_line": 956 + }, + { + "left_line": 963, + "right_line": 957 + }, + { + "left_line": 964, + "right_line": 958 + }, + { + "left_line": 965, + "right_line": 959 + }, + { + "left_line": 966, + "right_line": 960 + }, + { + "left_line": 967, + "right_line": 961 + }, + { + "left_line": 968, + "right_line": 962 + }, + { + "left_line": 969, + "right_line": 963 + }, + { + "left_line": 970, + "right_line": 964 + }, + { + "left_line": 971, + "right_line": 965 + }, + { + "left_line": 972, + "right_line": 966 + }, + { + "left_line": 973, + "right_line": 967 + }, + { + "left_line": 974, + "right_line": 968 + }, + { + "left_line": 975, + "right_line": 969 + }, + { + "left_line": 976, + "right_line": 970 + }, + { + "left_line": 977, + "right_line": 971 + }, + { + "left_line": 978, + "right_line": 972 + }, + { + "left_line": 979, + "right_line": 973 + }, + { + "left_line": 980, + "right_line": 974 + }, + { + "left_line": 981, + "right_line": 975 + }, + { + "left_line": 982, + "right_line": 976 + }, + { + "left_line": 983, + "right_line": 977 + }, + { + "left_line": 984, + "right_line": 978 + }, + { + "left_line": 985, + "right_line": 979 + }, + { + "left_line": 986, + "right_line": 980 + }, + { + "left_line": 987, + "right_line": 981 + }, + { + "left_line": 988, + "right_line": 982 + }, + { + "left_line": 989, + "right_line": 983 + }, + { + "left_line": 990, + "right_line": 984 + }, + { + "left_line": 991, + "right_line": 985 + }, + { + "left_line": 992, + "right_line": 986 + }, + { + "left_line": 993, + "right_line": 987 + }, + { + "left_line": 994, + "right_line": 988 + }, + { + "left_line": 995, + "right_line": 989 + }, + { + "left_line": 996, + "right_line": 990 + }, + { + "left_line": 997, + "right_line": 991 + }, + { + "left_line": 998, + "right_line": 992 + }, + { + "left_line": 999, + "right_line": 993 + }, + { + "left_line": 1000, + "right_line": 994 + }, + { + "left_line": 1001, + "right_line": 995 + }, + { + "left_line": 1002, + "right_line": 996 + }, + { + "left_line": 1003, + "right_line": 997 + }, + { + "left_line": 1004, + "right_line": 998 + }, + { + "left_line": 1005, + "right_line": 999 + }, + { + "left_line": 1006, + "right_line": 1000 + }, + { + "left_line": 1007, + "right_line": 1001 + }, + { + "left_line": 1008, + "right_line": 1002 + }, + { + "left_line": 1009, + "right_line": 1003 + }, + { + "left_line": 1010, + "right_line": 1004 + }, + { + "left_line": 1011, + "right_line": 1005 + }, + { + "left_line": 1012, + "right_line": 1006 + }, + { + "left_line": 1013, + "right_line": 1007 + }, + { + "left_line": 1014, + "right_line": 1008 + }, + { + "left_line": 1015, + "right_line": 1009 + }, + { + "left_line": 1016, + "right_line": 1010 + }, + { + "left_line": 1017, + "right_line": 1011 + }, + { + "left_line": 1018, + "right_line": 1012 + }, + { + "left_line": 1019, + "right_line": 1013 + }, + { + "left_line": 1020, + "right_line": 1014 + }, + { + "left_line": 1021, + "right_line": 1015 + }, + { + "left_line": 1022, + "right_line": 1016 + }, + { + "left_line": 1023, + "right_line": 1017 + }, + { + "left_line": 1024, + "right_line": 1018 + }, + { + "left_line": 1025, + "right_line": 1019 + }, + { + "left_line": 1026, + "right_line": 1020 + }, + { + "left_line": 1027, + "right_line": 1021 + }, + { + "left_line": 1028, + "right_line": 1022 + }, + { + "left_line": 1029, + "right_line": 1023 + }, + { + "left_line": 1030, + "right_line": 1024 + }, + { + "left_line": 1031, + "right_line": 1025 + }, + { + "left_line": 1032, + "right_line": 1026 + }, + { + "left_line": 1033, + "right_line": 1027 + }, + { + "left_line": 1034, + "right_line": 1028 + }, + { + "left_line": 1035, + "right_line": 1029 + }, + { + "left_line": 1036, + "right_line": 1030 + }, + { + "left_line": 1037, + "right_line": 1031 + }, + { + "left_line": 1038, + "right_line": 1032 + }, + { + "left_line": 1039, + "right_line": 1033 + }, + { + "left_line": 1040, + "right_line": 1034 + }, + { + "left_line": 1041, + "right_line": 1035 + }, + { + "left_line": 1042, + "right_line": 1036 + }, + { + "left_line": 1043, + "right_line": 1037 + }, + { + "left_line": 1044, + "right_line": 1038 + }, + { + "left_line": 1045, + "right_line": 1039 + }, + { + "left_line": 1046, + "right_line": 1040 + }, + { + "left_line": 1047, + "right_line": 1041 + }, + { + "left_line": 1048, + "right_line": 1042 + }, + { + "left_line": 1049, + "right_line": 1043 + }, + { + "left_line": 1050, + "right_line": 1044 + }, + { + "left_line": 1051, + "right_line": 1045 + }, + { + "left_line": 1052, + "right_line": 1046 + }, + { + "left_line": 1053, + "right_line": 1047 + }, + { + "left_line": 1054, + "right_line": 1048 + }, + { + "left_line": 1055, + "right_line": 1049 + }, + { + "left_line": 1056, + "right_line": 1050 + }, + { + "left_line": 1057, + "right_line": 1051 + }, + { + "left_line": 1058, + "right_line": 1052 + }, + { + "left_line": 1059, + "right_line": 1053 + }, + { + "left_line": 1060, + "right_line": 1054 + }, + { + "left_line": 1061, + "right_line": 1055 + }, + { + "left_line": 1062, + "right_line": 1056 + }, + { + "left_line": 1063, + "right_line": 1057 + }, + { + "left_line": 1064, + "right_line": 1058 + }, + { + "left_line": 1065, + "right_line": 1059 + }, + { + "left_line": 1066, + "right_line": 1060 + }, + { + "left_line": 1067, + "right_line": 1061 + }, + { + "left_line": 1068, + "right_line": 1062 + }, + { + "left_line": 1069, + "right_line": 1063 + }, + { + "left_line": 1070, + "right_line": 1064 + }, + { + "left_line": 1071, + "right_line": 1065 + }, + { + "left_line": 1072, + "right_line": 1066 + }, + { + "left_line": 1073, + "right_line": 1067 + }, + { + "left_line": 1074, + "right_line": 1068 + }, + { + "left_line": 1075, + "right_line": 1069 + }, + { + "left_line": 1076, + "right_line": 1070 + }, + { + "left_line": 1077, + "right_line": 1071 + }, + { + "left_line": 1078, + "right_line": 1072 + }, + { + "left_line": 1079, + "right_line": 1073 + }, + { + "left_line": 1080, + "right_line": 1074 + }, + { + "left_line": 1081, + "right_line": 1075 + }, + { + "left_line": 1082, + "right_line": 1076 + }, + { + "left_line": 1083, + "right_line": 1077 + }, + { + "left_line": 1084, + "right_line": 1078 + }, + { + "left_line": 1085, + "right_line": 1079 + }, + { + "left_line": 1086, + "right_line": 1080 + }, + { + "left_line": 1087, + "right_line": 1081 + }, + { + "left_line": 1088, + "right_line": 1082 + }, + { + "left_line": 1089, + "right_line": 1083 + }, + { + "left_line": 1090, + "right_line": 1084 + }, + { + "left_line": 1091, + "right_line": 1085 + }, + { + "left_line": 1092, + "right_line": 1086 + }, + { + "left_line": 1093, + "right_line": 1087 + }, + { + "left_line": 1094, + "right_line": 1088 + }, + { + "left_line": 1095, + "right_line": 1089 + }, + { + "left_line": 1096, + "right_line": 1090 + }, + { + "left_line": 1097, + "right_line": 1091 + }, + { + "left_line": 1098, + "right_line": 1092 + }, + { + "left_line": 1099, + "right_line": 1093 + }, + { + "left_line": 1100, + "right_line": 1094 + }, + { + "left_line": 1101, + "right_line": 1095 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_fs_api_refactor/metadata.json b/tests/testdata/lua_neovim_fs_api_refactor/metadata.json new file mode 100644 index 0000000..e58df08 --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_refactor/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "7d2249c579b0a9a19982170823675e25f3fba070", + "commit": "9a93ac65", + "message": "refactor(vim.fs): slug() minor cleanup", + "author": "Justin M. Keyes", + "date": "2026-08-05T19:49:08+02:00", + "language": "lua", + "files_changed": 1, + "insertions": 6, + "deletions": 12 +} diff --git a/tests/testdata/lua_neovim_fs_api_refactor/new.lua b/tests/testdata/lua_neovim_fs_api_refactor/new.lua new file mode 100644 index 0000000..6cd5f89 --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_refactor/new.lua @@ -0,0 +1,1095 @@ +--- @brief +--- [vim.fs.copy()]() +--- +--- Use |filecopy()| or |uv.fs_copyfile()| to performantly copy an existing file. +--- +--- Example: +--- +--- ```lua +--- vim.fn.filecopy('foo.txt', 'bar.txt') +--- ``` +--- +--- [vim.fs.exists()]() +--- +--- Use |uv.fs_stat()| to check a file's type, and whether it exists. +--- +--- Example: +--- +--- ```lua +--- if vim.uv.fs_stat(file) then +--- vim.print('file exists') +--- end +--- ``` +--- +--- [vim.fs.read()]() +--- +--- You can use |readblob()| to get a file's contents without explicitly opening/closing it. +--- Or use |io.lines()| to iterate lines in a text file. +--- +--- Example: +--- ```lua +--- vim.print(vim.fn.readblob('.git/config')) +--- ``` +--- +--- [vim.fs.write()]() +--- +--- You can use |writefile()| to write a file without explicitly opening/closing it. +--- +--- Example: +--- ```lua +--- vim.fn.writefile('foo\0bar', 'data.bin', 'b') +--- ``` + +local uv = vim.uv + +local M = {} + +local iswin = vim.fn.has('win32') == 1 +local os_sep = iswin and '\\' or '/' + +--- Iterate over all the parents of the given path (not expanded/resolved, the caller must do that). +--- +--- Example: +--- +--- ```lua +--- local root_dir +--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do +--- if vim.fn.isdirectory(dir .. '/.git') == 1 then +--- root_dir = dir +--- break +--- end +--- end +--- +--- if root_dir then +--- print('Found git repository at', root_dir) +--- end +--- ``` +--- +---@since 10 +---@param start (string) Initial path. +---@return fun(_, dir: string): string? # Iterator +---@return nil +---@return string|nil +function M.parents(start) + return function(_, dir) + local parent = M.dirname(dir) + if parent == dir then + return nil + end + + return parent + end, + nil, + start +end + +--- Gets the parent directory of the given path (not expanded/resolved, the caller must do that). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Parent directory of `file` +function M.dirname(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local dir = vim.fn.fnamemodify(file, ':h') + if iswin then + return (dir:gsub(os_sep, '/')) + end + return dir +end + +--- Gets the basename of the given path (not expanded/resolved). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Basename of `file` +function M.basename(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local name = vim.fn.fnamemodify(file, ':t') + if iswin then + return (name:gsub(os_sep, '/')) + end + return name +end + +--- Concatenates partial paths (one absolute or relative path followed by zero or more relative +--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are +--- replaced with forward-slashes. Empty segments are removed. Paths are not expanded/resolved. +--- +--- Examples: +--- - "foo/", "/bar" => "foo/bar" +--- - "", "after/plugin" => "after/plugin" +--- - Windows: "a\foo\", "\bar" => "a/foo/bar" +--- +---@since 12 +---@param ... string +---@return string +function M.joinpath(...) + local n = select('#', ...) + ---@type string[] + local segments = {} + for i = 1, n do + local s = select(i, ...) + if s and #s > 0 then + segments[#segments + 1] = s + end + end + + local path = table.concat(segments, '/') + + return (path:gsub(iswin and '[/\\][/\\]*' or '//+', '/')) +end + +--- Gets a filesystem-safe, mnemonic slug (readable prefix + short hash) of an arbitrary filepath or +--- other "identity string". +--- +--- - The input is normalized so equivalent paths produce the same result. +--- - A hash of the normalized input is appended to prevent collisions. +--- - Unsafe chars are replaced with "-". +--- - `$HOME` is replaced with "~". +--- - UNC paths (Windows) are prefixed with "=unc-". +--- - If `opts.maxlen` is exceeded, the result will be truncated to `{head}~~~{tail}-{hash8}`. +--- - If the sanitized name is empty, the reserved label `=special` will be used. +--- +--- Examples: +--- +--- ```lua +--- vim.print(vim.fs.slug('/tmp/test/foo.md')) --> "tmp-test-foo.md-{hash}" +--- vim.print(vim.fs.slug('C:/src/project/main.c')) --> "C--src-project-main.c-{hash}" +--- vim.print(vim.fs.slug(vim.fn.expand('~/file.txt'))) --> "~-file.txt-{hash}" +--- vim.print(vim.fs.slug('---')) --> "=special-{hash}" +--- vim.print(vim.fs.slug(('/a/very/long/path'):rep(10) .. '/file.txt', { maxlen = 60 })) +--- --> "a-very-long-~~~-path-a-very-long-path-file.txt-{hash}" +--- ``` +--- +---@since 15 +---@param path string Filepath (or other identity string). +---@param opts? table +--- - maxlen: (integer, default: 180) Max length (bytes) of the result. +---@return string # Filesystem-safe, mnemonic slug. +function M.slug(path, opts) + vim.validate('path', path, 'string') + opts = opts or {} + vim.validate('maxlen', opts.maxlen, function(v) + return type(v) == 'number' and v >= 8 + end, true, '`opts.maxlen` must be >= 8') + local maxlen = opts.maxlen or 180 + + -- Normalize before computing the hash so equivalent paths produce the same result + path = vim.fs.normalize(path, { plain = true }) + local s = path + + -- Replace $HOME with `~` + -- `fnamemodify` resolves relative paths against CWD, so only call it on absolute paths + if vim.startswith(s, '/') or (iswin and s:match('^%w:/')) then + s = vim.fn.fnamemodify(s, ':~') + end + + -- Replace UNC `//...` (Windows only) with `=unc-` + -- `//?/` and `//./` are NT namespace prefixes, not UNC + if + iswin + and vim.startswith(s, '//') + and not vim.startswith(s, '//?/') + and not vim.startswith(s, '//./') + then + s = '=unc-' .. s:sub(3) + end + + -- Sanitize unsafe chars and trim trailing "-" and "." + s = s:gsub('[%c%s/\\:*?"<>|]', '-') + s = s:gsub('[.-]+$', '') + -- Strip the leading "-" from an absolute path + s = s:gsub('^-', '') + + -- Always compute the hash to prevent collisions + local hash8 = vim.fn.sha256(path):sub(1, 8) + + -- Fully scrubbed path uses the reserved prefix + if s == '' then + s = '=special' + end + + -- Within maxlen: "{name}-{hash8}" + if #s + 1 + #hash8 <= maxlen then + return s .. '-' .. hash8 + end + + -- "{head}~~~{tail}-{hash8}" + local budget = maxlen - 12 -- 3 for "~~~", 1 for "-", 8 for hash + if budget < 1 then + -- No room for a readable form: degrade to a plain hash (maxlen >= 8 == #hash8). + return hash8 + end + local head_len = math.floor(budget / 3) + local h = s:sub(1, head_len):match('^.*()-') or head_len -- byte position where {head} ends + if h == head_len and h >= 1 then + -- No "-" found in prefix: ensure we don't split a UTF-8 character. + -- `vim.str_utf_start` returns an offset (<= 0) from the byte position to the character start. + -- `vim.str_utf_end` returns an offset (>= 0) to the character's last byte. + local char_start = h + vim.str_utf_start(s, h) ---@type integer + if char_start + vim.str_utf_end(s, char_start) > h then + h = char_start - 1 + end + end + -- Always in [h + 4, #s]: the "{name}-{hash8}" case above handled #s <= budget + 3. + local tail_start = #s - budget + h + 1 + local t = s:find('-', tail_start, true) or tail_start -- byte position where {tail} starts + -- If we fall back to a byte position, step forward past a split character + if t == tail_start then + local offset_start = vim.str_utf_start(s, t) + if offset_start < 0 then + local char_start = t + offset_start ---@type integer + t = char_start + vim.str_utf_end(s, char_start) + 1 + end + end + return s:sub(1, h) .. '~~~' .. s:sub(t) .. '-' .. hash8 +end + +--- Wrapper around `uv.fs_scandir_next()` that ensures a file type is returned. +--- +--- @param fs uv.uv_fs_t +--- @param path string +--- @return string? +--- @return string? +local function fs_scandir_next(fs, path) + -- use uv.fs_lstat instead of uv.fs_stat to avoid descending into a symlink entry as a directory/file + local name, etype = uv.fs_scandir_next(fs) + + if not name then + return + end + + if etype == nil then + local stat = vim.uv.fs_lstat(M.joinpath(path, name)) + -- Workaround #39612 https://github.com/luvit/luv/issues/660 + etype = stat and stat.type or 'unknown' + end + + return name, etype +end + +--- @class vim.fs.dir.Opts +--- @inlinedoc +--- +--- How deep to traverse. +--- (default: `1`) +--- @field depth? integer +--- +--- Report errors via the iterator's third value ("err"), instead of silently skipping. +--- (default: `false`) +--- @field err? boolean +--- +--- Predicate to control traversal. +--- Return false to stop searching the current directory. +--- Only useful when depth > 1 +--- Return an iterator over the items located in {path} +--- @field skip? (fun(dir_name: string): boolean) +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean +--- +--- Do not expand special forms like "~" and "$" in {path}. +--- (default: `false`) +--- @field plain? boolean + +--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|). +--- +--- Example: +--- +--- ```lua +--- for name, type, err in vim.fs.dir(path, { err = true }) do +--- if err then +--- -- Failed to scan directory {name} (may be the root {path} itself). +--- end +--- end +--- ``` +--- +---@since 10 +---@param path (string) Directory to iterate over, expanded (unless +--- `opts.plain=true`) and normalized. +---@param opts? vim.fs.dir.Opts +---@return fun(): string?, string?, string? # Iterator over items in {path}, yielding (name, type, err): +--- - name: Basename of the item relative to {path}. +--- - type: One of: "file", "directory", "link", "fifo", "socket", "char", "block", "unknown". +--- - err: Error string, or nil. Only if `opts.err=true`. If the root {path} itself could not +--- be scanned, yields a single (name, nil, err) item. +function M.dir(path, opts) + opts = opts or {} + + vim.validate('path', path, 'string') + vim.validate('depth', opts.depth, 'number', true) + vim.validate('err', opts.err, 'boolean', true) + vim.validate('follow', opts.follow, 'boolean', true) + vim.validate('skip', opts.skip, 'function', true) + vim.validate('plain', opts.plain, 'boolean', true) + + path = M.normalize(path, { plain = opts.plain }) + + local rootfs, rooterr = uv.fs_scandir(path) + + if not rootfs then + -- Root scan failed: + -- - If opts.err=false, behave as an empty listing (back-compat). + -- - If opts.err=true, surface yield a single (name, nil, err) result. + local done = not opts.err + return function() + if done then + return nil + end + done = true + return path, nil, rooterr + end + end + + if not opts.depth or opts.depth == 1 then + return function() + return fs_scandir_next(rootfs, path) + end + end + + --- @async + return coroutine.wrap(function() + local dirs = { { path, 1, rootfs } } + while #dirs > 0 do + --- @type string, integer, any + local dir0, level, fs = unpack(table.remove(dirs, 1)) + local dir = level == 1 and dir0 or M.joinpath(path, dir0) + while fs do + local name, t = fs_scandir_next(fs, dir) + if not name then + break + end + local f = level == 1 and name or M.joinpath(dir0, name) + local err_scan = nil + if + opts.depth + and level < opts.depth + and (t == 'directory' or (t == 'link' and opts.follow and ( + uv.fs_stat(M.joinpath(path, f)) or {} + ).type == 'directory')) + and (not opts.skip or opts.skip(f) ~= false) + then + local fs_next, err = uv.fs_scandir(M.joinpath(path, f)) + if not fs_next then + err_scan = opts.err and err or nil + else + dirs[#dirs + 1] = { f, level + 1, fs_next } + end + end + coroutine.yield(f, t, err_scan) + end + end + end) +end + +--- @class vim.fs.find.Opts +--- @inlinedoc +--- +--- Path to begin searching from, defaults to |current-directory|. Not expanded. +--- @field path? string +--- +--- Search upward through parent directories. Otherwise, search child directories (recursively). +--- (default: `false`) +--- @field upward? boolean +--- +--- Stop searching when this directory is reached. The directory itself is not searched. +--- @field stop? string +--- +--- Find only items of the given type. If omitted, all items that match {names} are included. +--- @field type? string +--- +--- Stop searching after this many matches. Use `math.huge` for "unlimited". +--- (default: `1`) +--- @field limit? number +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean + +--- Find files or directories (or other items as specified by `opts.type`) in the given path. +--- +--- Finds items given in {names} starting from {path}. If {upward} is "true" +--- then the search traverses upward through parent directories; otherwise, +--- the search traverses downward. Note that downward searches are recursive +--- and may search through many directories! If {stop} is non-nil, then the +--- search stops when the directory given in {stop} is reached. The search +--- terminates when {limit} (default 1) matches are found. You can set {type} +--- to "file", "directory", "link", "socket", "char", "block", or "fifo" +--- to narrow the search to find only that type. +--- +--- Examples: +--- +--- ```lua +--- -- List all test directories under the runtime directory. +--- local dirs = vim.fs.find( +--- { 'test', 'tst', 'testdir' }, +--- { limit = math.huge, type = 'directory', path = './runtime/' } +--- ) +--- +--- -- Get all "lib/*.cpp" and "lib/*.hpp" files, using Lua patterns. +--- -- Or use `vim.glob.to_lpeg(…):match(…)` for glob/wildcard matching. +--- local files = vim.fs.find(function(name, path) +--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$') +--- end, { limit = math.huge, type = 'file' }) +--- ``` +--- +---@since 10 +---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find. +--- Must be base names, paths and globs are not supported when {names} is a string or a table. +--- If {names} is a function, it is called for each traversed item with args: +--- - name: base name of the current item +--- - path: full path of the current item +--- +--- The function should return `true` if the given item is considered a match. +--- +---@param opts? vim.fs.find.Opts Optional keyword arguments: +---@return string[] # Normalized paths |vim.fs.normalize()| of all matching items. +---@return string[] # Errors collected while searching. +function M.find(names, opts) + opts = opts or {} + vim.validate('names', names, { 'string', 'table', 'function' }) + vim.validate('path', opts.path, 'string', true) + vim.validate('upward', opts.upward, 'boolean', true) + vim.validate('stop', opts.stop, 'string', true) + vim.validate('type', opts.type, 'string', true) + vim.validate('limit', opts.limit, 'number', true) + vim.validate('follow', opts.follow, 'boolean', true) + + if type(names) == 'string' then + names = { names } + end + + local path = opts.path or assert(uv.cwd()) + local stop = opts.stop + local limit = opts.limit or 1 + + local matches = {} --- @type string[] + local errors = {} --- @type string[] + + local function add(match) + matches[#matches + 1] = M.normalize(match) + if #matches == limit then + return true + end + end + + if opts.upward then + local test --- @type fun(p: string): string[] + + if type(names) == 'function' then + test = function(p) + local t = {} + for name, type, err in M.dir(p, { err = true }) do + if err ~= nil then + table.insert(errors, err) + elseif (not opts.type or opts.type == type) and names(name, p) then + table.insert(t, M.joinpath(p, name)) + end + end + return t + end + else + test = function(p) + local t = {} --- @type string[] + local ok, aerr = uv.fs_access(p, 'R') -- Check if the root dir is readable. + if not ok then + table.insert(errors, aerr) + return t + end + for _, name in ipairs(names) do + local f = M.joinpath(p, name) + local stat = uv.fs_stat(f) + if stat and (not opts.type or opts.type == stat.type) then + t[#t + 1] = f + end + end + + return t + end + end + + for _, match in ipairs(test(path)) do + if add(match) then + return matches, errors + end + end + + for parent in M.parents(path) do + if stop and parent == stop then + break + end + + for _, match in ipairs(test(parent)) do + if add(match) then + return matches, errors + end + end + end + else + local dirs = { path } + while #dirs > 0 do + local dir = table.remove(dirs, 1) + if stop and dir == stop then + break + end + + for other, type_, err in M.dir(dir, { err = true }) do + if err ~= nil then + table.insert(errors, err) + else + local f = M.joinpath(dir, other) + if type(names) == 'function' then + if (not opts.type or opts.type == type_) and names(other, dir) then + if add(f) then + return matches, errors + end + end + else + for _, name in ipairs(names) do + if name == other and (not opts.type or opts.type == type_) then + if add(f) then + return matches, errors + end + end + end + end + + if + type_ == 'directory' + or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory') + then + dirs[#dirs + 1] = f + end + end + end + end + end + + return matches, errors +end + +--- Find the first parent directory containing a specific "marker", relative to a file path or +--- buffer. +--- +--- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search +--- begins from Nvim's |current-directory|. +--- +--- Examples: +--- +--- ```lua +--- -- Find the root of a Python project, starting from file 'main.py' +--- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' }) +--- +--- -- Find the root of a git repository +--- vim.fs.root(0, '.git') +--- +--- -- Find the parent directory containing any file with a .csproj extension +--- vim.fs.root(0, function(name, path) +--- return vim.fs.ext(name) == 'csproj' +--- end) +--- +--- -- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if +--- -- not found, find the first ancestor containing ".git": +--- vim.fs.root(0, { { 'stylua.toml', '.luarc.json' }, '.git' }) +--- ``` +--- +--- @since 12 +--- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or +--- relative, expanded via `abspath()`) to begin the search from. +--- @param marker (string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean +--- Filename, function, or list thereof, that decides how to find the root. To +--- indicate "equal priority", specify items in a nested list `{ { 'a.txt', 'b.lua' }, … }`. +--- A function item must return true if `name` and `path` are a match. Each item +--- (which may itself be a nested list) is evaluated in-order against all ancestors, +--- until a match is found. +--- @return string? # Directory path containing one of the given markers, or nil if no directory was +--- found. +function M.root(source, marker) + assert(source, 'missing required argument: source') + assert(marker, 'missing required argument: marker') + + local path ---@type string + if type(source) == 'string' then + path = source + elseif type(source) == 'number' then + if vim.bo[source].buftype ~= '' then + path = assert(uv.cwd()) + else + path = vim.api.nvim_buf_get_name(source) + end + else + error('invalid type for argument "source": expected string or buffer number') + end + + local markers = type(marker) == 'table' and marker or { marker } + for _, mark in ipairs(markers) do + local paths = M.find(mark, { + upward = true, + path = M.abspath(path), + }) + + if #paths ~= 0 then + local dir = M.dirname(paths[1]) + return dir and M.abspath(dir) or nil + end + end + + return nil +end + +--- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX +--- path. The path must use forward slashes as path separator. +--- +--- Does not check if the path is a valid Windows path. Invalid paths will give invalid results. +--- +--- Examples: +--- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar` +--- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar` +--- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar` +--- - `C:/foo/bar` -> `C:`, `/foo/bar` +--- - `C:foo/bar` -> `C:`, `foo/bar` +--- +--- @param path string Path to split. +--- @return string, string, boolean : prefix, body, whether path is invalid. +local function split_windows_path(path) + local prefix = '' + + --- Match pattern. If there is a match, move the matched pattern from the path to the prefix. + --- Returns the matched pattern. + --- + --- @param pattern string Pattern to match. + --- @return string|nil Matched pattern + local function match_to_prefix(pattern) + local match = path:match(pattern) + + if match then + prefix = prefix .. match --[[ @as string ]] + path = path:sub(#match + 1) + end + + return match + end + + local function process_unc_path() + return match_to_prefix('[^/]+/+[^/]+/+') + end + + if match_to_prefix('^//[?.]/') then + -- Device paths + local device = match_to_prefix('[^/]+/+') + + -- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path + if not device or (device:match('^UNC/+$') and not process_unc_path()) then + return prefix, path, false + end + elseif match_to_prefix('^//') then + -- Process UNC path, return early if it's invalid + if not process_unc_path() then + return prefix, path, false + end + elseif path:match('^%w:') then + -- Drive paths + prefix, path = path:sub(1, 2), path:sub(3) + end + + -- If there are slashes at the end of the prefix, move them to the start of the body. This is to + -- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no + -- slashes at the end of the prefix, so it will be treated as a relative path, as it should be. + local trailing_slash = prefix:match('/+$') + + if trailing_slash then + prefix = prefix:sub(1, -1 - #trailing_slash) + path = trailing_slash .. path --[[ @as string ]] + end + + return prefix, path, true +end + +--- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes. +--- `..` is not resolved if the path is relative and resolving it requires the path to be absolute. +--- If a relative path resolves to the current directory, an empty string is returned. +--- +--- @see M.normalize() +--- @param path string Path to resolve. +--- @return string Resolved path. +local function path_resolve_dot(path) + local is_path_absolute = vim.startswith(path, '/') + local new_path_components = {} + + for component in vim.gsplit(path, '/') do + if component == '.' or component == '' then -- luacheck: ignore 542 + -- Skip `.` components and empty components + elseif component == '..' then + if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then + -- For `..`, remove the last component if we're still inside the current directory, except + -- when the last component is `..` itself + table.remove(new_path_components) + elseif is_path_absolute then -- luacheck: ignore 542 + -- Reached the root directory in absolute path, do nothing + else + -- Reached current directory in relative path, add `..` to the path + table.insert(new_path_components, component) + end + else + table.insert(new_path_components, component) + end + end + + return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/') +end + +--- Expand tilde (~) character at the beginning of the path to the user's home directory. +--- +--- @param path string Path to expand. +--- @param sep string|nil Path separator to use. Uses os_sep by default. +--- @return string Expanded path. +local function expand_home(path, sep) + sep = sep or os_sep + + if vim.startswith(path, '~') then + local home = uv.os_homedir() or '~' --- @type string + + if home:sub(-1) == sep then + home = home:sub(1, -2) + end + + path = home .. path:sub(2) --- @type string + end + + return path +end + +--- @class vim.fs.normalize.Opts +--- @inlinedoc +--- +--- Expand environment variables (deprecated). +--- (default: `true`) +--- @field package expand_env? boolean +--- +--- Do not expand environment variables and leading tildes "~". +--- (default: `false`) +--- @field plain? boolean +--- +--- @field package _fast? boolean +--- +--- Path is a Windows path. +--- (default: `true` in Windows, `false` otherwise) +--- @field win? boolean + +--- Normalize a path to a standard format. Expands environment variables, and tilde "~" at the +--- beginning of the path. Resolves "." and ".." components, except when the path is relative and +--- resolving it would produce an absolute path. +--- +--- - "." as the only part in a relative path: +--- - "." => "." +--- - "././" => "." +--- - ".." when it leads outside the current directory +--- - "foo/../../bar" => "../bar" +--- - "../../foo" => "../../foo" +--- - ".." in the root directory returns the root directory. +--- - "/../../" => "/" +--- +--- On Windows, backslashes (`\`) are converted to forward slashes (`/`). +--- +--- Examples: +--- ```lua +--- [[C:\Users\jdoe]] --> "C:/Users/jdoe" +--- "~/src/neovim" --> "/home/jdoe/src/neovim" +--- "$XDG_CONFIG_HOME/nvim/init.vim" --> "/Users/jdoe/.config/nvim/init.vim" +--- "~/src/nvim/api/../tui/./tui.c" --> "/home/jdoe/src/nvim/tui/tui.c" +--- "./foo/bar" --> "foo/bar" +--- "foo/../../../bar" --> "../../bar" +--- "/home/jdoe/../../../bar" --> "/bar" +--- "C:foo/../../baz" --> "C:../baz" +--- "C:/foo/../../baz" --> "C:/baz" +--- [[\\?\UNC\server\share\foo\..\..\..\bar]] --> "//?/UNC/server/share/bar" +--- ``` +--- +---@since 10 +---@param path (string) Path to normalize +---@param opts? vim.fs.normalize.Opts +---@return (string) : Normalized path +function M.normalize(path, opts) + opts = opts or {} + + if not opts._fast then + vim.validate('path', path, 'string') + vim.validate('plain', opts.plain, 'boolean', true) + vim.validate('win', opts.win, 'boolean', true) + end + + local win = opts.win == nil and iswin or not not opts.win + local os_sep_local = win and '\\' or '/' + + -- Empty path is already normalized + if path == '' then + return '' + end + + if not opts.plain then + -- Expand ~ to user's home directory + path = expand_home(path, os_sep_local) + + -- Expand environment variables + if opts.expand_env == nil or opts.expand_env then + path = path:gsub('%$([%w_]+)', uv.os_getenv) --- @type string + end + end + + if win then + -- Convert path separator to `/` + path = path:gsub(os_sep_local, '/') --- @type string + end + + -- Check for double slashes at the start of the path because they have special meaning + local double_slash = false + if not opts._fast then + double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///') + end + + local prefix = '' + + if win then + local is_valid --- @type boolean + -- Split Windows paths into prefix and body to make processing easier + prefix, path, is_valid = split_windows_path(path) + + -- If path is not valid, return it as-is + if not is_valid then + return prefix .. path + end + + -- Ensure capital drive and remove extraneous slashes from the prefix + prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/') + end + + if not opts._fast then + -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix + -- and path. + path = path_resolve_dot(path) + end + + -- Preserve leading double slashes as they indicate UNC paths and DOS device paths in + -- Windows and have implementation-defined behavior in POSIX. + path = (double_slash and '/' or '') .. prefix .. path + + -- Change empty path to `.` + if path == '' then + path = '.' + end + + return path +end + +--- @class vim.fs.mkdir.Opts +--- @inlinedoc +--- +--- Create intermediate directories as necessary. +--- (default: `false`) +--- @field parents? boolean +--- +--- Permission bits for newly-created directories. +--- (default: `493`) +--- @field mode? integer + +--- Creates a directory. +--- +---@since 15 +---@param path string Path to create (not expanded/resolved). +---@param opts? vim.fs.mkdir.Opts Optional keyword arguments. +function M.mkdir(path, opts) + vim.validate('path', path, 'string') + vim.validate('opts', opts, 'table', true) + opts = opts or {} + vim.validate('parents', opts.parents, 'boolean', true) + vim.validate('mode', opts.mode, 'number', true) + + vim.fn.mkdir(path, opts.parents and 'p' or '', tostring(opts.mode or 493)) +end + +--- @param path string Path to remove +--- @param ty string type of path +--- @param recursive? boolean +--- @param force? boolean +local function rm(path, ty, recursive, force) + --- @diagnostic disable-next-line:no-unknown + local rm_fn + + if ty == 'directory' then + if recursive then + for file, fty in vim.fs.dir(path) do + rm(M.joinpath(path, file), fty, true, force) + end + elseif not force then + error(string.format('%s is a directory', path)) + end + + rm_fn = uv.fs_rmdir + else + rm_fn = uv.fs_unlink + end + + local ret, err, errnm = rm_fn(path) + if ret == nil and (not force or errnm ~= 'ENOENT') then + error(err) + end +end + +--- @class vim.fs.rm.Opts +--- @inlinedoc +--- +--- Remove directory contents recursively. +--- @field recursive? boolean +--- +--- Ignore nonexistent files and arguments. +--- @field force? boolean + +--- Removes a file or directory. +--- +--- Removes symlinks without touching the origin. To remove the origin, resolve it explicitly +--- with |uv.fs_realpath()|: +--- ```lua +--- vim.fs.rm(vim.uv.fs_realpath('symlink-dir'), { recursive = true }) +--- ``` +--- +--- @since 13 +--- @param path string Path to remove (not expanded/resolved). +--- @param opts? vim.fs.rm.Opts +function M.rm(path, opts) + opts = opts or {} + + local stat, err, errnm = uv.fs_lstat(path) + if stat then + rm(path, stat.type, opts.recursive, opts.force) + elseif not opts.force or errnm ~= 'ENOENT' then + error(err) + end +end + +--- @class vim.fs.abspath.Opts +--- @inlinedoc +--- +--- Resolve the path relative to this directory. +--- @field cwd? string +--- +--- Do not expand tilde (~). +--- @field plain? boolean + +--- Converts `path` to an absolute path. Expands tilde (~) at the beginning of the path (unless +--- plain=true). Does not check if the path exists, normalize the path, resolve symlinks or +--- hardlinks (including "." and ".."), or expand environment variables. If the path is already +--- absolute, it is returned unchanged. Converts `\` path separators to `/`. +--- +--- @since 13 +--- @param path string Path +--- @param opts? vim.fs.abspath.Opts +--- @return string Absolute path +function M.abspath(path, opts) + -- TODO(justinmk): mark f_fnamemodify as API_FAST and use it, ":p:h" should be safe... + -- + opts = opts or {} + + vim.validate('path', path, 'string') + vim.validate('cwd', opts.cwd, 'string', true) + vim.validate('plain', opts.plain, 'boolean', true) + + -- Expand ~ to user's home directory + if not opts.plain then + path = expand_home(path) + end + + -- Convert path separator to `/` + path = path:gsub(os_sep, '/') + + local prefix = '' + + if iswin then + prefix, path = split_windows_path(path) + end + + if prefix == '//' or vim.startswith(path, '/') then + -- Path is already absolute, do nothing + return prefix .. path + end + + -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory + -- of the drive specified in the path + local cwd = + assert((iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or opts.cwd or uv.cwd()) + -- Convert cwd path separator to `/` + cwd = cwd:gsub(os_sep, '/') + + if path == '.' then + return cwd + end + -- Prefix is not needed for expanding relative paths, `cwd` already contains it. + return M.joinpath(cwd, path) +end + +--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor. +--- +--- Example: +--- +--- ```lua +--- vim.fs.relpath('/var', '/var/lib') -- 'lib' +--- vim.fs.relpath('/var', '/usr/bin') -- nil +--- ``` +--- +--- @since 13 +--- @param base string +--- @param target string +--- @param opts table? Reserved for future use +--- @return string|nil +function M.relpath(base, target, opts) + vim.validate('base', base, 'string') + vim.validate('target', target, 'string') + vim.validate('opts', opts, 'table', true) + + base = M.normalize(M.abspath(base)) + target = M.normalize(M.abspath(target)) + if base == target then + return '.' + end + + local prefix = '' + if iswin then + prefix, base = split_windows_path(base) + end + base = prefix .. base .. (base ~= '/' and '/' or '') + + return vim.startswith(target, base) and target:sub(#base + 1) or nil +end + +--- Return the file's last extension, if any. +--- +--- Similar to |fnamemodify()| with the |::e| modifier. The extension does not include a leading +--- period. +--- +--- Examples: +--- +--- ```lua +--- vim.fs.ext('archive.tar.gz') -- 'gz' +--- vim.fs.ext('~/.git') -- '' +--- vim.fs.ext('plugin/myplug.lua') -- 'lua' +--- ``` +--- +---@since 14 +---@param file string Path +---@param opts table? Reserved for future use +---@return string Extension of {file} +function M.ext(file, opts) + vim.validate('file', file, 'string') + vim.validate('opts', opts, 'table', true) + return vim.fn.fnamemodify(file, ':e') +end + +return M diff --git a/tests/testdata/lua_neovim_fs_api_refactor/old.lua b/tests/testdata/lua_neovim_fs_api_refactor/old.lua new file mode 100644 index 0000000..ec21ae3 --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_refactor/old.lua @@ -0,0 +1,1101 @@ +--- @brief +--- [vim.fs.copy()]() +--- +--- Use |filecopy()| or |uv.fs_copyfile()| to performantly copy an existing file. +--- +--- Example: +--- +--- ```lua +--- vim.fn.filecopy('foo.txt', 'bar.txt') +--- ``` +--- +--- [vim.fs.exists()]() +--- +--- Use |uv.fs_stat()| to check a file's type, and whether it exists. +--- +--- Example: +--- +--- ```lua +--- if vim.uv.fs_stat(file) then +--- vim.print('file exists') +--- end +--- ``` +--- +--- [vim.fs.read()]() +--- +--- You can use |readblob()| to get a file's contents without explicitly opening/closing it. +--- Or use |io.lines()| to iterate lines in a text file. +--- +--- Example: +--- ```lua +--- vim.print(vim.fn.readblob('.git/config')) +--- ``` +--- +--- [vim.fs.write()]() +--- +--- You can use |writefile()| to write a file without explicitly opening/closing it. +--- +--- Example: +--- ```lua +--- vim.fn.writefile('foo\0bar', 'data.bin', 'b') +--- ``` + +local uv = vim.uv + +local M = {} + +local iswin = vim.fn.has('win32') == 1 +local os_sep = iswin and '\\' or '/' + +--- Iterate over all the parents of the given path (not expanded/resolved, the caller must do that). +--- +--- Example: +--- +--- ```lua +--- local root_dir +--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do +--- if vim.fn.isdirectory(dir .. '/.git') == 1 then +--- root_dir = dir +--- break +--- end +--- end +--- +--- if root_dir then +--- print('Found git repository at', root_dir) +--- end +--- ``` +--- +---@since 10 +---@param start (string) Initial path. +---@return fun(_, dir: string): string? # Iterator +---@return nil +---@return string|nil +function M.parents(start) + return function(_, dir) + local parent = M.dirname(dir) + if parent == dir then + return nil + end + + return parent + end, + nil, + start +end + +--- Gets the parent directory of the given path (not expanded/resolved, the caller must do that). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Parent directory of `file` +function M.dirname(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local dir = vim.fn.fnamemodify(file, ':h') + if iswin then + return (dir:gsub(os_sep, '/')) + end + return dir +end + +--- Gets the basename of the given path (not expanded/resolved). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Basename of `file` +function M.basename(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local name = vim.fn.fnamemodify(file, ':t') + if iswin then + return (name:gsub(os_sep, '/')) + end + return name +end + +--- Concatenates partial paths (one absolute or relative path followed by zero or more relative +--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are +--- replaced with forward-slashes. Empty segments are removed. Paths are not expanded/resolved. +--- +--- Examples: +--- - "foo/", "/bar" => "foo/bar" +--- - "", "after/plugin" => "after/plugin" +--- - Windows: "a\foo\", "\bar" => "a/foo/bar" +--- +---@since 12 +---@param ... string +---@return string +function M.joinpath(...) + local n = select('#', ...) + ---@type string[] + local segments = {} + for i = 1, n do + local s = select(i, ...) + if s and #s > 0 then + segments[#segments + 1] = s + end + end + + local path = table.concat(segments, '/') + + return (path:gsub(iswin and '[/\\][/\\]*' or '//+', '/')) +end + +--- Gets a filesystem-safe, mnemonic slug (readable prefix + short hash) of an arbitrary filepath or +--- other "identity string". +--- +--- - The input is normalized so equivalent paths produce the same result. +--- - A hash of the normalized input is appended to prevent collisions. +--- - Unsafe chars are replaced with "-". +--- - `$HOME` is replaced with "~". +--- - UNC paths (Windows) are prefixed with "=unc-". +--- - If `opts.maxlen` is exceeded, the result will be truncated to `{head}~~~{tail}-{hash8}`. +--- - If the sanitized name is empty, the reserved label `=special` will be used. +--- +--- Examples: +--- +--- ```lua +--- vim.print(vim.fs.slug('/tmp/test/foo.md')) --> "tmp-test-foo.md-{hash}" +--- vim.print(vim.fs.slug('C:/src/project/main.c')) --> "C--src-project-main.c-{hash}" +--- vim.print(vim.fs.slug(vim.fn.expand('~/file.txt'))) --> "~-file.txt-{hash}" +--- vim.print(vim.fs.slug('---')) --> "=special-{hash}" +--- vim.print(vim.fs.slug(('/a/very/long/path'):rep(10) .. '/file.txt', { maxlen = 60 })) +--- --> "a-very-long-~~~-path-a-very-long-path-file.txt-{hash}" +--- ``` +--- +---@since 15 +---@param path string Filepath (or other identity string). +---@param opts? table +--- - maxlen: (integer, default: 180) Max length (bytes) of the result. +---@return string # Filesystem-safe, mnemonic slug. +function M.slug(path, opts) + vim.validate('path', path, 'string') + opts = opts or {} + vim.validate('maxlen', opts.maxlen, function(v) + if v == nil then + return true + end + return type(v) == 'number' and v >= 8 + end, '`opt.maxlen` must be >= 8') + opts.maxlen = opts.maxlen or 180 + + -- Normalize before computing the hash so equivalent paths produce the same result + path = vim.fs.normalize(path, { plain = true }) + local s = path + + -- Replace $HOME with `~` + -- `fnamemodify` resolves relative paths against CWD, so only call it on absolute paths + if vim.startswith(s, '/') or (iswin and s:match('^%w:/')) then + s = vim.fn.fnamemodify(s, ':~') + end + + -- Replace UNC `//...` (Windows only) with `=unc-` + -- `//?/` and `//./` are NT namespace prefixes, not UNC + if + iswin + and vim.startswith(s, '//') + and not vim.startswith(s, '//?/') + and not vim.startswith(s, '//./') + then + s = '=unc-' .. s:sub(3) + end + + -- Sanitize unsafe chars and trim trailing "-" and "." + s = s:gsub('[%c%s/\\:*?"<>|]', '-') + s = s:gsub('[.-]+$', '') + -- Strip the leading "-" from an absolute path + s = s:gsub('^-', '') + + -- Always compute the hash to prevent collisions + local hash8 = vim.fn.sha256(path):sub(1, 8) + + -- Fully scrubbed path uses the reserved prefix + if s == '' then + s = '=special' + end + + -- Within maxlen: "{name}-{hash8}" + local maxlen = opts.maxlen + if #s + 1 + #hash8 <= maxlen then + return s .. '-' .. hash8 + end + + -- "{head}~~~{tail}-{hash8}" + local budget = maxlen - 12 -- 3 for "~~~", 1 for "-", 8 for hash + if budget < 1 then + -- No room for a readable form: degrade to a plain hash + return hash8:sub(1, maxlen) + end + local head_len = math.floor(budget / 3) + local h = s:sub(1, head_len):match('^.*()-') or head_len -- byte position where {head} ends + if h == head_len and h >= 1 then + -- No "-" found in prefix: ensure we don't split a UTF-8 character. + -- `vim.str_utf_start` returns an offset (<= 0) from the byte position to the character start. + -- `vim.str_utf_end` returns an offset (>= 0) to the character's last byte. + local char_start = h + vim.str_utf_start(s, h) ---@type integer + if char_start + vim.str_utf_end(s, char_start) > h then + h = char_start - 1 + end + end + local tail_start = #s - budget + h + 1 + if tail_start < 1 then + tail_start = 1 + end + local t = s:find('-', tail_start, true) or tail_start -- byte position where {tail} starts + -- If we fall back to a byte position, step forward past a split character + if t == tail_start and t <= #s then + local offset_start = vim.str_utf_start(s, t) + if offset_start < 0 then + local char_start = t + offset_start ---@type integer + t = char_start + vim.str_utf_end(s, char_start) + 1 + end + end + return s:sub(1, h) .. '~~~' .. s:sub(t) .. '-' .. hash8 +end + +--- Wrapper around `uv.fs_scandir_next()` that ensures a file type is returned. +--- +--- @param fs uv.uv_fs_t +--- @param path string +--- @return string? +--- @return string? +local function fs_scandir_next(fs, path) + -- use uv.fs_lstat instead of uv.fs_stat to avoid descending into a symlink entry as a directory/file + local name, etype = uv.fs_scandir_next(fs) + + if not name then + return + end + + if etype == nil then + local stat = vim.uv.fs_lstat(M.joinpath(path, name)) + -- Workaround #39612 https://github.com/luvit/luv/issues/660 + etype = stat and stat.type or 'unknown' + end + + return name, etype +end + +--- @class vim.fs.dir.Opts +--- @inlinedoc +--- +--- How deep to traverse. +--- (default: `1`) +--- @field depth? integer +--- +--- Report errors via the iterator's third value ("err"), instead of silently skipping. +--- (default: `false`) +--- @field err? boolean +--- +--- Predicate to control traversal. +--- Return false to stop searching the current directory. +--- Only useful when depth > 1 +--- Return an iterator over the items located in {path} +--- @field skip? (fun(dir_name: string): boolean) +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean +--- +--- Do not expand special forms like "~" and "$" in {path}. +--- (default: `false`) +--- @field plain? boolean + +--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|). +--- +--- Example: +--- +--- ```lua +--- for name, type, err in vim.fs.dir(path, { err = true }) do +--- if err then +--- -- Failed to scan directory {name} (may be the root {path} itself). +--- end +--- end +--- ``` +--- +---@since 10 +---@param path (string) Directory to iterate over, expanded (unless +--- `opts.plain=true`) and normalized. +---@param opts? vim.fs.dir.Opts +---@return fun(): string?, string?, string? # Iterator over items in {path}, yielding (name, type, err): +--- - name: Basename of the item relative to {path}. +--- - type: One of: "file", "directory", "link", "fifo", "socket", "char", "block", "unknown". +--- - err: Error string, or nil. Only if `opts.err=true`. If the root {path} itself could not +--- be scanned, yields a single (name, nil, err) item. +function M.dir(path, opts) + opts = opts or {} + + vim.validate('path', path, 'string') + vim.validate('depth', opts.depth, 'number', true) + vim.validate('err', opts.err, 'boolean', true) + vim.validate('follow', opts.follow, 'boolean', true) + vim.validate('skip', opts.skip, 'function', true) + vim.validate('plain', opts.plain, 'boolean', true) + + path = M.normalize(path, { plain = opts.plain }) + + local rootfs, rooterr = uv.fs_scandir(path) + + if not rootfs then + -- Root scan failed: + -- - If opts.err=false, behave as an empty listing (back-compat). + -- - If opts.err=true, surface yield a single (name, nil, err) result. + local done = not opts.err + return function() + if done then + return nil + end + done = true + return path, nil, rooterr + end + end + + if not opts.depth or opts.depth == 1 then + return function() + return fs_scandir_next(rootfs, path) + end + end + + --- @async + return coroutine.wrap(function() + local dirs = { { path, 1, rootfs } } + while #dirs > 0 do + --- @type string, integer, any + local dir0, level, fs = unpack(table.remove(dirs, 1)) + local dir = level == 1 and dir0 or M.joinpath(path, dir0) + while fs do + local name, t = fs_scandir_next(fs, dir) + if not name then + break + end + local f = level == 1 and name or M.joinpath(dir0, name) + local err_scan = nil + if + opts.depth + and level < opts.depth + and (t == 'directory' or (t == 'link' and opts.follow and ( + uv.fs_stat(M.joinpath(path, f)) or {} + ).type == 'directory')) + and (not opts.skip or opts.skip(f) ~= false) + then + local fs_next, err = uv.fs_scandir(M.joinpath(path, f)) + if not fs_next then + err_scan = opts.err and err or nil + else + dirs[#dirs + 1] = { f, level + 1, fs_next } + end + end + coroutine.yield(f, t, err_scan) + end + end + end) +end + +--- @class vim.fs.find.Opts +--- @inlinedoc +--- +--- Path to begin searching from, defaults to |current-directory|. Not expanded. +--- @field path? string +--- +--- Search upward through parent directories. Otherwise, search child directories (recursively). +--- (default: `false`) +--- @field upward? boolean +--- +--- Stop searching when this directory is reached. The directory itself is not searched. +--- @field stop? string +--- +--- Find only items of the given type. If omitted, all items that match {names} are included. +--- @field type? string +--- +--- Stop searching after this many matches. Use `math.huge` for "unlimited". +--- (default: `1`) +--- @field limit? number +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean + +--- Find files or directories (or other items as specified by `opts.type`) in the given path. +--- +--- Finds items given in {names} starting from {path}. If {upward} is "true" +--- then the search traverses upward through parent directories; otherwise, +--- the search traverses downward. Note that downward searches are recursive +--- and may search through many directories! If {stop} is non-nil, then the +--- search stops when the directory given in {stop} is reached. The search +--- terminates when {limit} (default 1) matches are found. You can set {type} +--- to "file", "directory", "link", "socket", "char", "block", or "fifo" +--- to narrow the search to find only that type. +--- +--- Examples: +--- +--- ```lua +--- -- List all test directories under the runtime directory. +--- local dirs = vim.fs.find( +--- { 'test', 'tst', 'testdir' }, +--- { limit = math.huge, type = 'directory', path = './runtime/' } +--- ) +--- +--- -- Get all "lib/*.cpp" and "lib/*.hpp" files, using Lua patterns. +--- -- Or use `vim.glob.to_lpeg(…):match(…)` for glob/wildcard matching. +--- local files = vim.fs.find(function(name, path) +--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$') +--- end, { limit = math.huge, type = 'file' }) +--- ``` +--- +---@since 10 +---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find. +--- Must be base names, paths and globs are not supported when {names} is a string or a table. +--- If {names} is a function, it is called for each traversed item with args: +--- - name: base name of the current item +--- - path: full path of the current item +--- +--- The function should return `true` if the given item is considered a match. +--- +---@param opts? vim.fs.find.Opts Optional keyword arguments: +---@return string[] # Normalized paths |vim.fs.normalize()| of all matching items. +---@return string[] # Errors collected while searching. +function M.find(names, opts) + opts = opts or {} + vim.validate('names', names, { 'string', 'table', 'function' }) + vim.validate('path', opts.path, 'string', true) + vim.validate('upward', opts.upward, 'boolean', true) + vim.validate('stop', opts.stop, 'string', true) + vim.validate('type', opts.type, 'string', true) + vim.validate('limit', opts.limit, 'number', true) + vim.validate('follow', opts.follow, 'boolean', true) + + if type(names) == 'string' then + names = { names } + end + + local path = opts.path or assert(uv.cwd()) + local stop = opts.stop + local limit = opts.limit or 1 + + local matches = {} --- @type string[] + local errors = {} --- @type string[] + + local function add(match) + matches[#matches + 1] = M.normalize(match) + if #matches == limit then + return true + end + end + + if opts.upward then + local test --- @type fun(p: string): string[] + + if type(names) == 'function' then + test = function(p) + local t = {} + for name, type, err in M.dir(p, { err = true }) do + if err ~= nil then + table.insert(errors, err) + elseif (not opts.type or opts.type == type) and names(name, p) then + table.insert(t, M.joinpath(p, name)) + end + end + return t + end + else + test = function(p) + local t = {} --- @type string[] + local ok, aerr = uv.fs_access(p, 'R') -- Check if the root dir is readable. + if not ok then + table.insert(errors, aerr) + return t + end + for _, name in ipairs(names) do + local f = M.joinpath(p, name) + local stat = uv.fs_stat(f) + if stat and (not opts.type or opts.type == stat.type) then + t[#t + 1] = f + end + end + + return t + end + end + + for _, match in ipairs(test(path)) do + if add(match) then + return matches, errors + end + end + + for parent in M.parents(path) do + if stop and parent == stop then + break + end + + for _, match in ipairs(test(parent)) do + if add(match) then + return matches, errors + end + end + end + else + local dirs = { path } + while #dirs > 0 do + local dir = table.remove(dirs, 1) + if stop and dir == stop then + break + end + + for other, type_, err in M.dir(dir, { err = true }) do + if err ~= nil then + table.insert(errors, err) + else + local f = M.joinpath(dir, other) + if type(names) == 'function' then + if (not opts.type or opts.type == type_) and names(other, dir) then + if add(f) then + return matches, errors + end + end + else + for _, name in ipairs(names) do + if name == other and (not opts.type or opts.type == type_) then + if add(f) then + return matches, errors + end + end + end + end + + if + type_ == 'directory' + or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory') + then + dirs[#dirs + 1] = f + end + end + end + end + end + + return matches, errors +end + +--- Find the first parent directory containing a specific "marker", relative to a file path or +--- buffer. +--- +--- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search +--- begins from Nvim's |current-directory|. +--- +--- Examples: +--- +--- ```lua +--- -- Find the root of a Python project, starting from file 'main.py' +--- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' }) +--- +--- -- Find the root of a git repository +--- vim.fs.root(0, '.git') +--- +--- -- Find the parent directory containing any file with a .csproj extension +--- vim.fs.root(0, function(name, path) +--- return vim.fs.ext(name) == 'csproj' +--- end) +--- +--- -- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if +--- -- not found, find the first ancestor containing ".git": +--- vim.fs.root(0, { { 'stylua.toml', '.luarc.json' }, '.git' }) +--- ``` +--- +--- @since 12 +--- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or +--- relative, expanded via `abspath()`) to begin the search from. +--- @param marker (string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean +--- Filename, function, or list thereof, that decides how to find the root. To +--- indicate "equal priority", specify items in a nested list `{ { 'a.txt', 'b.lua' }, … }`. +--- A function item must return true if `name` and `path` are a match. Each item +--- (which may itself be a nested list) is evaluated in-order against all ancestors, +--- until a match is found. +--- @return string? # Directory path containing one of the given markers, or nil if no directory was +--- found. +function M.root(source, marker) + assert(source, 'missing required argument: source') + assert(marker, 'missing required argument: marker') + + local path ---@type string + if type(source) == 'string' then + path = source + elseif type(source) == 'number' then + if vim.bo[source].buftype ~= '' then + path = assert(uv.cwd()) + else + path = vim.api.nvim_buf_get_name(source) + end + else + error('invalid type for argument "source": expected string or buffer number') + end + + local markers = type(marker) == 'table' and marker or { marker } + for _, mark in ipairs(markers) do + local paths = M.find(mark, { + upward = true, + path = M.abspath(path), + }) + + if #paths ~= 0 then + local dir = M.dirname(paths[1]) + return dir and M.abspath(dir) or nil + end + end + + return nil +end + +--- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX +--- path. The path must use forward slashes as path separator. +--- +--- Does not check if the path is a valid Windows path. Invalid paths will give invalid results. +--- +--- Examples: +--- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar` +--- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar` +--- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar` +--- - `C:/foo/bar` -> `C:`, `/foo/bar` +--- - `C:foo/bar` -> `C:`, `foo/bar` +--- +--- @param path string Path to split. +--- @return string, string, boolean : prefix, body, whether path is invalid. +local function split_windows_path(path) + local prefix = '' + + --- Match pattern. If there is a match, move the matched pattern from the path to the prefix. + --- Returns the matched pattern. + --- + --- @param pattern string Pattern to match. + --- @return string|nil Matched pattern + local function match_to_prefix(pattern) + local match = path:match(pattern) + + if match then + prefix = prefix .. match --[[ @as string ]] + path = path:sub(#match + 1) + end + + return match + end + + local function process_unc_path() + return match_to_prefix('[^/]+/+[^/]+/+') + end + + if match_to_prefix('^//[?.]/') then + -- Device paths + local device = match_to_prefix('[^/]+/+') + + -- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path + if not device or (device:match('^UNC/+$') and not process_unc_path()) then + return prefix, path, false + end + elseif match_to_prefix('^//') then + -- Process UNC path, return early if it's invalid + if not process_unc_path() then + return prefix, path, false + end + elseif path:match('^%w:') then + -- Drive paths + prefix, path = path:sub(1, 2), path:sub(3) + end + + -- If there are slashes at the end of the prefix, move them to the start of the body. This is to + -- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no + -- slashes at the end of the prefix, so it will be treated as a relative path, as it should be. + local trailing_slash = prefix:match('/+$') + + if trailing_slash then + prefix = prefix:sub(1, -1 - #trailing_slash) + path = trailing_slash .. path --[[ @as string ]] + end + + return prefix, path, true +end + +--- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes. +--- `..` is not resolved if the path is relative and resolving it requires the path to be absolute. +--- If a relative path resolves to the current directory, an empty string is returned. +--- +--- @see M.normalize() +--- @param path string Path to resolve. +--- @return string Resolved path. +local function path_resolve_dot(path) + local is_path_absolute = vim.startswith(path, '/') + local new_path_components = {} + + for component in vim.gsplit(path, '/') do + if component == '.' or component == '' then -- luacheck: ignore 542 + -- Skip `.` components and empty components + elseif component == '..' then + if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then + -- For `..`, remove the last component if we're still inside the current directory, except + -- when the last component is `..` itself + table.remove(new_path_components) + elseif is_path_absolute then -- luacheck: ignore 542 + -- Reached the root directory in absolute path, do nothing + else + -- Reached current directory in relative path, add `..` to the path + table.insert(new_path_components, component) + end + else + table.insert(new_path_components, component) + end + end + + return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/') +end + +--- Expand tilde (~) character at the beginning of the path to the user's home directory. +--- +--- @param path string Path to expand. +--- @param sep string|nil Path separator to use. Uses os_sep by default. +--- @return string Expanded path. +local function expand_home(path, sep) + sep = sep or os_sep + + if vim.startswith(path, '~') then + local home = uv.os_homedir() or '~' --- @type string + + if home:sub(-1) == sep then + home = home:sub(1, -2) + end + + path = home .. path:sub(2) --- @type string + end + + return path +end + +--- @class vim.fs.normalize.Opts +--- @inlinedoc +--- +--- Expand environment variables (deprecated). +--- (default: `true`) +--- @field package expand_env? boolean +--- +--- Do not expand environment variables and leading tildes "~". +--- (default: `false`) +--- @field plain? boolean +--- +--- @field package _fast? boolean +--- +--- Path is a Windows path. +--- (default: `true` in Windows, `false` otherwise) +--- @field win? boolean + +--- Normalize a path to a standard format. Expands environment variables, and tilde "~" at the +--- beginning of the path. Resolves "." and ".." components, except when the path is relative and +--- resolving it would produce an absolute path. +--- +--- - "." as the only part in a relative path: +--- - "." => "." +--- - "././" => "." +--- - ".." when it leads outside the current directory +--- - "foo/../../bar" => "../bar" +--- - "../../foo" => "../../foo" +--- - ".." in the root directory returns the root directory. +--- - "/../../" => "/" +--- +--- On Windows, backslashes (`\`) are converted to forward slashes (`/`). +--- +--- Examples: +--- ```lua +--- [[C:\Users\jdoe]] --> "C:/Users/jdoe" +--- "~/src/neovim" --> "/home/jdoe/src/neovim" +--- "$XDG_CONFIG_HOME/nvim/init.vim" --> "/Users/jdoe/.config/nvim/init.vim" +--- "~/src/nvim/api/../tui/./tui.c" --> "/home/jdoe/src/nvim/tui/tui.c" +--- "./foo/bar" --> "foo/bar" +--- "foo/../../../bar" --> "../../bar" +--- "/home/jdoe/../../../bar" --> "/bar" +--- "C:foo/../../baz" --> "C:../baz" +--- "C:/foo/../../baz" --> "C:/baz" +--- [[\\?\UNC\server\share\foo\..\..\..\bar]] --> "//?/UNC/server/share/bar" +--- ``` +--- +---@since 10 +---@param path (string) Path to normalize +---@param opts? vim.fs.normalize.Opts +---@return (string) : Normalized path +function M.normalize(path, opts) + opts = opts or {} + + if not opts._fast then + vim.validate('path', path, 'string') + vim.validate('plain', opts.plain, 'boolean', true) + vim.validate('win', opts.win, 'boolean', true) + end + + local win = opts.win == nil and iswin or not not opts.win + local os_sep_local = win and '\\' or '/' + + -- Empty path is already normalized + if path == '' then + return '' + end + + if not opts.plain then + -- Expand ~ to user's home directory + path = expand_home(path, os_sep_local) + + -- Expand environment variables + if opts.expand_env == nil or opts.expand_env then + path = path:gsub('%$([%w_]+)', uv.os_getenv) --- @type string + end + end + + if win then + -- Convert path separator to `/` + path = path:gsub(os_sep_local, '/') --- @type string + end + + -- Check for double slashes at the start of the path because they have special meaning + local double_slash = false + if not opts._fast then + double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///') + end + + local prefix = '' + + if win then + local is_valid --- @type boolean + -- Split Windows paths into prefix and body to make processing easier + prefix, path, is_valid = split_windows_path(path) + + -- If path is not valid, return it as-is + if not is_valid then + return prefix .. path + end + + -- Ensure capital drive and remove extraneous slashes from the prefix + prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/') + end + + if not opts._fast then + -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix + -- and path. + path = path_resolve_dot(path) + end + + -- Preserve leading double slashes as they indicate UNC paths and DOS device paths in + -- Windows and have implementation-defined behavior in POSIX. + path = (double_slash and '/' or '') .. prefix .. path + + -- Change empty path to `.` + if path == '' then + path = '.' + end + + return path +end + +--- @class vim.fs.mkdir.Opts +--- @inlinedoc +--- +--- Create intermediate directories as necessary. +--- (default: `false`) +--- @field parents? boolean +--- +--- Permission bits for newly-created directories. +--- (default: `493`) +--- @field mode? integer + +--- Creates a directory. +--- +---@since 15 +---@param path string Path to create (not expanded/resolved). +---@param opts? vim.fs.mkdir.Opts Optional keyword arguments. +function M.mkdir(path, opts) + vim.validate('path', path, 'string') + vim.validate('opts', opts, 'table', true) + opts = opts or {} + vim.validate('parents', opts.parents, 'boolean', true) + vim.validate('mode', opts.mode, 'number', true) + + vim.fn.mkdir(path, opts.parents and 'p' or '', tostring(opts.mode or 493)) +end + +--- @param path string Path to remove +--- @param ty string type of path +--- @param recursive? boolean +--- @param force? boolean +local function rm(path, ty, recursive, force) + --- @diagnostic disable-next-line:no-unknown + local rm_fn + + if ty == 'directory' then + if recursive then + for file, fty in vim.fs.dir(path) do + rm(M.joinpath(path, file), fty, true, force) + end + elseif not force then + error(string.format('%s is a directory', path)) + end + + rm_fn = uv.fs_rmdir + else + rm_fn = uv.fs_unlink + end + + local ret, err, errnm = rm_fn(path) + if ret == nil and (not force or errnm ~= 'ENOENT') then + error(err) + end +end + +--- @class vim.fs.rm.Opts +--- @inlinedoc +--- +--- Remove directory contents recursively. +--- @field recursive? boolean +--- +--- Ignore nonexistent files and arguments. +--- @field force? boolean + +--- Removes a file or directory. +--- +--- Removes symlinks without touching the origin. To remove the origin, resolve it explicitly +--- with |uv.fs_realpath()|: +--- ```lua +--- vim.fs.rm(vim.uv.fs_realpath('symlink-dir'), { recursive = true }) +--- ``` +--- +--- @since 13 +--- @param path string Path to remove (not expanded/resolved). +--- @param opts? vim.fs.rm.Opts +function M.rm(path, opts) + opts = opts or {} + + local stat, err, errnm = uv.fs_lstat(path) + if stat then + rm(path, stat.type, opts.recursive, opts.force) + elseif not opts.force or errnm ~= 'ENOENT' then + error(err) + end +end + +--- @class vim.fs.abspath.Opts +--- @inlinedoc +--- +--- Resolve the path relative to this directory. +--- @field cwd? string +--- +--- Do not expand tilde (~). +--- @field plain? boolean + +--- Converts `path` to an absolute path. Expands tilde (~) at the beginning of the path (unless +--- plain=true). Does not check if the path exists, normalize the path, resolve symlinks or +--- hardlinks (including "." and ".."), or expand environment variables. If the path is already +--- absolute, it is returned unchanged. Converts `\` path separators to `/`. +--- +--- @since 13 +--- @param path string Path +--- @param opts? vim.fs.abspath.Opts +--- @return string Absolute path +function M.abspath(path, opts) + -- TODO(justinmk): mark f_fnamemodify as API_FAST and use it, ":p:h" should be safe... + -- + opts = opts or {} + + vim.validate('path', path, 'string') + vim.validate('cwd', opts.cwd, 'string', true) + vim.validate('plain', opts.plain, 'boolean', true) + + -- Expand ~ to user's home directory + if not opts.plain then + path = expand_home(path) + end + + -- Convert path separator to `/` + path = path:gsub(os_sep, '/') + + local prefix = '' + + if iswin then + prefix, path = split_windows_path(path) + end + + if prefix == '//' or vim.startswith(path, '/') then + -- Path is already absolute, do nothing + return prefix .. path + end + + -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory + -- of the drive specified in the path + local cwd = + assert((iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or opts.cwd or uv.cwd()) + -- Convert cwd path separator to `/` + cwd = cwd:gsub(os_sep, '/') + + if path == '.' then + return cwd + end + -- Prefix is not needed for expanding relative paths, `cwd` already contains it. + return M.joinpath(cwd, path) +end + +--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor. +--- +--- Example: +--- +--- ```lua +--- vim.fs.relpath('/var', '/var/lib') -- 'lib' +--- vim.fs.relpath('/var', '/usr/bin') -- nil +--- ``` +--- +--- @since 13 +--- @param base string +--- @param target string +--- @param opts table? Reserved for future use +--- @return string|nil +function M.relpath(base, target, opts) + vim.validate('base', base, 'string') + vim.validate('target', target, 'string') + vim.validate('opts', opts, 'table', true) + + base = M.normalize(M.abspath(base)) + target = M.normalize(M.abspath(target)) + if base == target then + return '.' + end + + local prefix = '' + if iswin then + prefix, base = split_windows_path(base) + end + base = prefix .. base .. (base ~= '/' and '/' or '') + + return vim.startswith(target, base) and target:sub(#base + 1) or nil +end + +--- Return the file's last extension, if any. +--- +--- Similar to |fnamemodify()| with the |::e| modifier. The extension does not include a leading +--- period. +--- +--- Examples: +--- +--- ```lua +--- vim.fs.ext('archive.tar.gz') -- 'gz' +--- vim.fs.ext('~/.git') -- '' +--- vim.fs.ext('plugin/myplug.lua') -- 'lua' +--- ``` +--- +---@since 14 +---@param file string Path +---@param opts table? Reserved for future use +---@return string Extension of {file} +function M.ext(file, opts) + vim.validate('file', file, 'string') + vim.validate('opts', opts, 'table', true) + return vim.fn.fnamemodify(file, ':e') +end + +return M diff --git a/tests/testdata/lua_neovim_fs_api_small/expected.json b/tests/testdata/lua_neovim_fs_api_small/expected.json new file mode 100644 index 0000000..811e06a --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_small/expected.json @@ -0,0 +1,4089 @@ +{ + "version": "v1", + "actions": [ + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 45 + ], + "type": "comment", + "start_byte": 990, + "end_byte": 1090 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29450 + }, + "position": 40, + "old_parent": { + "tree": "before", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29604 + }, + "old_position": 45, + "dest_start_byte": 871, + "dest_end_byte": 878, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 69 + ], + "type": "comment", + "start_byte": 1735, + "end_byte": 1832 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29450 + }, + "position": 67, + "old_parent": { + "tree": "before", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29604 + }, + "old_position": 69, + "dest_start_byte": 1526, + "dest_end_byte": 1547, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 76 + ], + "type": "comment", + "start_byte": 2168, + "end_byte": 2232 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29450 + }, + "position": 74, + "old_parent": { + "tree": "before", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29604 + }, + "old_position": 76, + "dest_start_byte": 1899, + "dest_end_byte": 1940, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 83 + ], + "type": "comment", + "start_byte": 2564, + "end_byte": 2659 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29450 + }, + "position": 81, + "old_parent": { + "tree": "before", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29604 + }, + "old_position": 83, + "dest_start_byte": 2299, + "dest_end_byte": 2332, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 96 + ], + "type": "comment", + "start_byte": 3380, + "end_byte": 3459 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29450 + }, + "position": 94, + "old_parent": { + "tree": "before", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29604 + }, + "old_position": 96, + "dest_start_byte": 3036, + "dest_end_byte": 3053, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 103 + ], + "type": "comment", + "start_byte": 4002, + "end_byte": 4028 + }, + "parent": { + "tree": "after", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29450 + }, + "position": 101, + "old_parent": { + "tree": "before", + "path": null, + "type": "chunk", + "start_byte": 0, + "end_byte": 29604 + }, + "old_position": 103, + "dest_start_byte": 3532, + "dest_end_byte": 3551, + "group_id": "group-1" + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 0 + ], + "type": "comment", + "start_byte": 0, + "end_byte": 10 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": -1 + }, + { + "left_line": 154, + "right_line": 153 + }, + { + "left_line": -1, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": -1 + }, + { + "left_line": 194, + "right_line": 193 + }, + { + "left_line": 195, + "right_line": 194 + }, + { + "left_line": 196, + "right_line": 195 + }, + { + "left_line": 197, + "right_line": 196 + }, + { + "left_line": 198, + "right_line": 197 + }, + { + "left_line": 199, + "right_line": 198 + }, + { + "left_line": 200, + "right_line": 199 + }, + { + "left_line": 201, + "right_line": 200 + }, + { + "left_line": 202, + "right_line": 201 + }, + { + "left_line": 203, + "right_line": 202 + }, + { + "left_line": 204, + "right_line": 203 + }, + { + "left_line": 205, + "right_line": 204 + }, + { + "left_line": 206, + "right_line": 205 + }, + { + "left_line": 207, + "right_line": 206 + }, + { + "left_line": 208, + "right_line": 207 + }, + { + "left_line": 209, + "right_line": 208 + }, + { + "left_line": 210, + "right_line": 209 + }, + { + "left_line": 211, + "right_line": 210 + }, + { + "left_line": 212, + "right_line": 211 + }, + { + "left_line": 213, + "right_line": 212 + }, + { + "left_line": 214, + "right_line": 213 + }, + { + "left_line": 215, + "right_line": 214 + }, + { + "left_line": 216, + "right_line": 215 + }, + { + "left_line": 217, + "right_line": 216 + }, + { + "left_line": 218, + "right_line": 217 + }, + { + "left_line": 219, + "right_line": 218 + }, + { + "left_line": 220, + "right_line": 219 + }, + { + "left_line": 221, + "right_line": 220 + }, + { + "left_line": 222, + "right_line": 221 + }, + { + "left_line": 223, + "right_line": 222 + }, + { + "left_line": 224, + "right_line": 223 + }, + { + "left_line": 225, + "right_line": 224 + }, + { + "left_line": 226, + "right_line": 225 + }, + { + "left_line": 227, + "right_line": 226 + }, + { + "left_line": 228, + "right_line": 227 + }, + { + "left_line": 229, + "right_line": 228 + }, + { + "left_line": 230, + "right_line": 229 + }, + { + "left_line": 231, + "right_line": 230 + }, + { + "left_line": 232, + "right_line": 231 + }, + { + "left_line": 233, + "right_line": 232 + }, + { + "left_line": 234, + "right_line": 233 + }, + { + "left_line": 235, + "right_line": 234 + }, + { + "left_line": 236, + "right_line": 235 + }, + { + "left_line": 237, + "right_line": 236 + }, + { + "left_line": 238, + "right_line": 237 + }, + { + "left_line": 239, + "right_line": 238 + }, + { + "left_line": 240, + "right_line": 239 + }, + { + "left_line": 241, + "right_line": 240 + }, + { + "left_line": 242, + "right_line": 241 + }, + { + "left_line": 243, + "right_line": 242 + }, + { + "left_line": 244, + "right_line": 243 + }, + { + "left_line": 245, + "right_line": 244 + }, + { + "left_line": 246, + "right_line": 245 + }, + { + "left_line": 247, + "right_line": 246 + }, + { + "left_line": 248, + "right_line": 247 + }, + { + "left_line": 249, + "right_line": 248 + }, + { + "left_line": 250, + "right_line": 249 + }, + { + "left_line": 251, + "right_line": 250 + }, + { + "left_line": 252, + "right_line": 251 + }, + { + "left_line": 253, + "right_line": 252 + }, + { + "left_line": 254, + "right_line": 253 + }, + { + "left_line": 255, + "right_line": 254 + }, + { + "left_line": 256, + "right_line": 255 + }, + { + "left_line": 257, + "right_line": 256 + }, + { + "left_line": 258, + "right_line": 257 + }, + { + "left_line": 259, + "right_line": 258 + }, + { + "left_line": 260, + "right_line": 259 + }, + { + "left_line": 261, + "right_line": 260 + }, + { + "left_line": 262, + "right_line": 261 + }, + { + "left_line": 263, + "right_line": 262 + }, + { + "left_line": 264, + "right_line": 263 + }, + { + "left_line": 265, + "right_line": 264 + }, + { + "left_line": 266, + "right_line": 265 + }, + { + "left_line": 267, + "right_line": 266 + }, + { + "left_line": 268, + "right_line": 267 + }, + { + "left_line": 269, + "right_line": 268 + }, + { + "left_line": 270, + "right_line": 269 + }, + { + "left_line": 271, + "right_line": 270 + }, + { + "left_line": 272, + "right_line": 271 + }, + { + "left_line": 273, + "right_line": 272 + }, + { + "left_line": 274, + "right_line": 273 + }, + { + "left_line": 275, + "right_line": 274 + }, + { + "left_line": 276, + "right_line": 275 + }, + { + "left_line": 277, + "right_line": 276 + }, + { + "left_line": 278, + "right_line": 277 + }, + { + "left_line": 279, + "right_line": 278 + }, + { + "left_line": 280, + "right_line": 279 + }, + { + "left_line": 281, + "right_line": 280 + }, + { + "left_line": 282, + "right_line": 281 + }, + { + "left_line": 283, + "right_line": 282 + }, + { + "left_line": 284, + "right_line": 283 + }, + { + "left_line": 285, + "right_line": 284 + }, + { + "left_line": 286, + "right_line": 285 + }, + { + "left_line": 287, + "right_line": 286 + }, + { + "left_line": 288, + "right_line": 287 + }, + { + "left_line": 289, + "right_line": 288 + }, + { + "left_line": 290, + "right_line": 289 + }, + { + "left_line": 291, + "right_line": 290 + }, + { + "left_line": 292, + "right_line": 291 + }, + { + "left_line": 293, + "right_line": 292 + }, + { + "left_line": 294, + "right_line": 293 + }, + { + "left_line": 295, + "right_line": 294 + }, + { + "left_line": 296, + "right_line": 295 + }, + { + "left_line": 297, + "right_line": 296 + }, + { + "left_line": 298, + "right_line": 297 + }, + { + "left_line": 299, + "right_line": 298 + }, + { + "left_line": 300, + "right_line": 299 + }, + { + "left_line": 301, + "right_line": 300 + }, + { + "left_line": 302, + "right_line": 301 + }, + { + "left_line": 303, + "right_line": 302 + }, + { + "left_line": 304, + "right_line": 303 + }, + { + "left_line": 305, + "right_line": 304 + }, + { + "left_line": 306, + "right_line": 305 + }, + { + "left_line": 307, + "right_line": 306 + }, + { + "left_line": 308, + "right_line": 307 + }, + { + "left_line": 309, + "right_line": 308 + }, + { + "left_line": 310, + "right_line": 309 + }, + { + "left_line": 311, + "right_line": 310 + }, + { + "left_line": 312, + "right_line": 311 + }, + { + "left_line": 313, + "right_line": 312 + }, + { + "left_line": 314, + "right_line": 313 + }, + { + "left_line": 315, + "right_line": 314 + }, + { + "left_line": 316, + "right_line": 315 + }, + { + "left_line": 317, + "right_line": 316 + }, + { + "left_line": 318, + "right_line": 317 + }, + { + "left_line": 319, + "right_line": 318 + }, + { + "left_line": 320, + "right_line": 319 + }, + { + "left_line": 321, + "right_line": 320 + }, + { + "left_line": 322, + "right_line": 321 + }, + { + "left_line": 323, + "right_line": 322 + }, + { + "left_line": 324, + "right_line": 323 + }, + { + "left_line": 325, + "right_line": 324 + }, + { + "left_line": 326, + "right_line": 325 + }, + { + "left_line": 327, + "right_line": 326 + }, + { + "left_line": 328, + "right_line": 327 + }, + { + "left_line": 329, + "right_line": 328 + }, + { + "left_line": 330, + "right_line": 329 + }, + { + "left_line": 331, + "right_line": 330 + }, + { + "left_line": 332, + "right_line": 331 + }, + { + "left_line": 333, + "right_line": 332 + }, + { + "left_line": 334, + "right_line": 333 + }, + { + "left_line": 335, + "right_line": 334 + }, + { + "left_line": 336, + "right_line": 335 + }, + { + "left_line": 337, + "right_line": 336 + }, + { + "left_line": 338, + "right_line": 337 + }, + { + "left_line": 339, + "right_line": 338 + }, + { + "left_line": 340, + "right_line": 339 + }, + { + "left_line": 341, + "right_line": 340 + }, + { + "left_line": 342, + "right_line": 341 + }, + { + "left_line": 343, + "right_line": 342 + }, + { + "left_line": 344, + "right_line": 343 + }, + { + "left_line": 345, + "right_line": 344 + }, + { + "left_line": 346, + "right_line": 345 + }, + { + "left_line": 347, + "right_line": 346 + }, + { + "left_line": 348, + "right_line": 347 + }, + { + "left_line": 349, + "right_line": 348 + }, + { + "left_line": 350, + "right_line": 349 + }, + { + "left_line": 351, + "right_line": 350 + }, + { + "left_line": 352, + "right_line": 351 + }, + { + "left_line": 353, + "right_line": 352 + }, + { + "left_line": 354, + "right_line": 353 + }, + { + "left_line": 355, + "right_line": 354 + }, + { + "left_line": 356, + "right_line": 355 + }, + { + "left_line": 357, + "right_line": 356 + }, + { + "left_line": 358, + "right_line": 357 + }, + { + "left_line": 359, + "right_line": 358 + }, + { + "left_line": 360, + "right_line": 359 + }, + { + "left_line": 361, + "right_line": 360 + }, + { + "left_line": 362, + "right_line": 361 + }, + { + "left_line": 363, + "right_line": 362 + }, + { + "left_line": 364, + "right_line": 363 + }, + { + "left_line": 365, + "right_line": 364 + }, + { + "left_line": 366, + "right_line": 365 + }, + { + "left_line": 367, + "right_line": 366 + }, + { + "left_line": 368, + "right_line": 367 + }, + { + "left_line": 369, + "right_line": 368 + }, + { + "left_line": 370, + "right_line": 369 + }, + { + "left_line": 371, + "right_line": 370 + }, + { + "left_line": 372, + "right_line": 371 + }, + { + "left_line": 373, + "right_line": 372 + }, + { + "left_line": 374, + "right_line": 373 + }, + { + "left_line": 375, + "right_line": 374 + }, + { + "left_line": 376, + "right_line": 375 + }, + { + "left_line": 377, + "right_line": 376 + }, + { + "left_line": 378, + "right_line": 377 + }, + { + "left_line": 379, + "right_line": 378 + }, + { + "left_line": 380, + "right_line": 379 + }, + { + "left_line": 381, + "right_line": 380 + }, + { + "left_line": 382, + "right_line": 381 + }, + { + "left_line": 383, + "right_line": 382 + }, + { + "left_line": 384, + "right_line": 383 + }, + { + "left_line": 385, + "right_line": 384 + }, + { + "left_line": 386, + "right_line": 385 + }, + { + "left_line": 387, + "right_line": 386 + }, + { + "left_line": 388, + "right_line": 387 + }, + { + "left_line": 389, + "right_line": 388 + }, + { + "left_line": 390, + "right_line": 389 + }, + { + "left_line": 391, + "right_line": 390 + }, + { + "left_line": 392, + "right_line": 391 + }, + { + "left_line": 393, + "right_line": 392 + }, + { + "left_line": 394, + "right_line": 393 + }, + { + "left_line": 395, + "right_line": 394 + }, + { + "left_line": 396, + "right_line": 395 + }, + { + "left_line": 397, + "right_line": 396 + }, + { + "left_line": 398, + "right_line": 397 + }, + { + "left_line": 399, + "right_line": 398 + }, + { + "left_line": 400, + "right_line": 399 + }, + { + "left_line": 401, + "right_line": 400 + }, + { + "left_line": 402, + "right_line": 401 + }, + { + "left_line": 403, + "right_line": 402 + }, + { + "left_line": 404, + "right_line": 403 + }, + { + "left_line": 405, + "right_line": 404 + }, + { + "left_line": 406, + "right_line": 405 + }, + { + "left_line": 407, + "right_line": 406 + }, + { + "left_line": 408, + "right_line": 407 + }, + { + "left_line": 409, + "right_line": 408 + }, + { + "left_line": 410, + "right_line": 409 + }, + { + "left_line": 411, + "right_line": 410 + }, + { + "left_line": 412, + "right_line": 411 + }, + { + "left_line": 413, + "right_line": 412 + }, + { + "left_line": 414, + "right_line": 413 + }, + { + "left_line": 415, + "right_line": 414 + }, + { + "left_line": 416, + "right_line": 415 + }, + { + "left_line": 417, + "right_line": 416 + }, + { + "left_line": 418, + "right_line": 417 + }, + { + "left_line": 419, + "right_line": 418 + }, + { + "left_line": 420, + "right_line": 419 + }, + { + "left_line": 421, + "right_line": 420 + }, + { + "left_line": 422, + "right_line": 421 + }, + { + "left_line": 423, + "right_line": 422 + }, + { + "left_line": 424, + "right_line": 423 + }, + { + "left_line": 425, + "right_line": 424 + }, + { + "left_line": 426, + "right_line": 425 + }, + { + "left_line": 427, + "right_line": 426 + }, + { + "left_line": 428, + "right_line": 427 + }, + { + "left_line": 429, + "right_line": 428 + }, + { + "left_line": 430, + "right_line": 429 + }, + { + "left_line": 431, + "right_line": 430 + }, + { + "left_line": 432, + "right_line": 431 + }, + { + "left_line": 433, + "right_line": 432 + }, + { + "left_line": 434, + "right_line": 433 + }, + { + "left_line": 435, + "right_line": 434 + }, + { + "left_line": 436, + "right_line": 435 + }, + { + "left_line": 437, + "right_line": 436 + }, + { + "left_line": 438, + "right_line": 437 + }, + { + "left_line": 439, + "right_line": 438 + }, + { + "left_line": 440, + "right_line": 439 + }, + { + "left_line": 441, + "right_line": 440 + }, + { + "left_line": 442, + "right_line": 441 + }, + { + "left_line": 443, + "right_line": 442 + }, + { + "left_line": 444, + "right_line": 443 + }, + { + "left_line": 445, + "right_line": 444 + }, + { + "left_line": 446, + "right_line": 445 + }, + { + "left_line": 447, + "right_line": 446 + }, + { + "left_line": 448, + "right_line": 447 + }, + { + "left_line": 449, + "right_line": 448 + }, + { + "left_line": 450, + "right_line": 449 + }, + { + "left_line": 451, + "right_line": 450 + }, + { + "left_line": 452, + "right_line": 451 + }, + { + "left_line": 453, + "right_line": 452 + }, + { + "left_line": 454, + "right_line": 453 + }, + { + "left_line": 455, + "right_line": 454 + }, + { + "left_line": 456, + "right_line": 455 + }, + { + "left_line": 457, + "right_line": 456 + }, + { + "left_line": 458, + "right_line": 457 + }, + { + "left_line": 459, + "right_line": 458 + }, + { + "left_line": 460, + "right_line": 459 + }, + { + "left_line": 461, + "right_line": 460 + }, + { + "left_line": 462, + "right_line": 461 + }, + { + "left_line": 463, + "right_line": 462 + }, + { + "left_line": 464, + "right_line": 463 + }, + { + "left_line": 465, + "right_line": 464 + }, + { + "left_line": 466, + "right_line": 465 + }, + { + "left_line": 467, + "right_line": 466 + }, + { + "left_line": 468, + "right_line": 467 + }, + { + "left_line": 469, + "right_line": 468 + }, + { + "left_line": 470, + "right_line": 469 + }, + { + "left_line": 471, + "right_line": 470 + }, + { + "left_line": 472, + "right_line": 471 + }, + { + "left_line": 473, + "right_line": 472 + }, + { + "left_line": 474, + "right_line": 473 + }, + { + "left_line": 475, + "right_line": 474 + }, + { + "left_line": 476, + "right_line": 475 + }, + { + "left_line": 477, + "right_line": 476 + }, + { + "left_line": 478, + "right_line": 477 + }, + { + "left_line": 479, + "right_line": 478 + }, + { + "left_line": 480, + "right_line": 479 + }, + { + "left_line": 481, + "right_line": 480 + }, + { + "left_line": 482, + "right_line": 481 + }, + { + "left_line": 483, + "right_line": 482 + }, + { + "left_line": 484, + "right_line": 483 + }, + { + "left_line": 485, + "right_line": 484 + }, + { + "left_line": 486, + "right_line": 485 + }, + { + "left_line": 487, + "right_line": 486 + }, + { + "left_line": 488, + "right_line": 487 + }, + { + "left_line": 489, + "right_line": 488 + }, + { + "left_line": 490, + "right_line": 489 + }, + { + "left_line": 491, + "right_line": 490 + }, + { + "left_line": 492, + "right_line": 491 + }, + { + "left_line": 493, + "right_line": 492 + }, + { + "left_line": 494, + "right_line": 493 + }, + { + "left_line": 495, + "right_line": 494 + }, + { + "left_line": 496, + "right_line": 495 + }, + { + "left_line": 497, + "right_line": 496 + }, + { + "left_line": 498, + "right_line": 497 + }, + { + "left_line": 499, + "right_line": 498 + }, + { + "left_line": 500, + "right_line": 499 + }, + { + "left_line": 501, + "right_line": 500 + }, + { + "left_line": 502, + "right_line": 501 + }, + { + "left_line": 503, + "right_line": 502 + }, + { + "left_line": 504, + "right_line": 503 + }, + { + "left_line": 505, + "right_line": 504 + }, + { + "left_line": 506, + "right_line": 505 + }, + { + "left_line": 507, + "right_line": 506 + }, + { + "left_line": 508, + "right_line": 507 + }, + { + "left_line": 509, + "right_line": 508 + }, + { + "left_line": 510, + "right_line": 509 + }, + { + "left_line": 511, + "right_line": 510 + }, + { + "left_line": 512, + "right_line": 511 + }, + { + "left_line": 513, + "right_line": 512 + }, + { + "left_line": 514, + "right_line": 513 + }, + { + "left_line": 515, + "right_line": 514 + }, + { + "left_line": 516, + "right_line": 515 + }, + { + "left_line": 517, + "right_line": 516 + }, + { + "left_line": 518, + "right_line": 517 + }, + { + "left_line": 519, + "right_line": 518 + }, + { + "left_line": 520, + "right_line": 519 + }, + { + "left_line": 521, + "right_line": 520 + }, + { + "left_line": 522, + "right_line": 521 + }, + { + "left_line": 523, + "right_line": 522 + }, + { + "left_line": 524, + "right_line": 523 + }, + { + "left_line": 525, + "right_line": 524 + }, + { + "left_line": 526, + "right_line": 525 + }, + { + "left_line": 527, + "right_line": 526 + }, + { + "left_line": 528, + "right_line": 527 + }, + { + "left_line": 529, + "right_line": 528 + }, + { + "left_line": 530, + "right_line": 529 + }, + { + "left_line": 531, + "right_line": 530 + }, + { + "left_line": 532, + "right_line": 531 + }, + { + "left_line": 533, + "right_line": 532 + }, + { + "left_line": 534, + "right_line": 533 + }, + { + "left_line": 535, + "right_line": 534 + }, + { + "left_line": 536, + "right_line": 535 + }, + { + "left_line": 537, + "right_line": 536 + }, + { + "left_line": 538, + "right_line": 537 + }, + { + "left_line": 539, + "right_line": 538 + }, + { + "left_line": 540, + "right_line": 539 + }, + { + "left_line": 541, + "right_line": 540 + }, + { + "left_line": 542, + "right_line": 541 + }, + { + "left_line": 543, + "right_line": 542 + }, + { + "left_line": 544, + "right_line": 543 + }, + { + "left_line": 545, + "right_line": 544 + }, + { + "left_line": 546, + "right_line": 545 + }, + { + "left_line": 547, + "right_line": 546 + }, + { + "left_line": 548, + "right_line": 547 + }, + { + "left_line": 549, + "right_line": 548 + }, + { + "left_line": 550, + "right_line": 549 + }, + { + "left_line": 551, + "right_line": 550 + }, + { + "left_line": 552, + "right_line": 551 + }, + { + "left_line": 553, + "right_line": 552 + }, + { + "left_line": 554, + "right_line": 553 + }, + { + "left_line": 555, + "right_line": 554 + }, + { + "left_line": 556, + "right_line": 555 + }, + { + "left_line": 557, + "right_line": 556 + }, + { + "left_line": 558, + "right_line": 557 + }, + { + "left_line": 559, + "right_line": 558 + }, + { + "left_line": 560, + "right_line": 559 + }, + { + "left_line": 561, + "right_line": 560 + }, + { + "left_line": 562, + "right_line": 561 + }, + { + "left_line": 563, + "right_line": 562 + }, + { + "left_line": 564, + "right_line": 563 + }, + { + "left_line": 565, + "right_line": 564 + }, + { + "left_line": 566, + "right_line": 565 + }, + { + "left_line": 567, + "right_line": 566 + }, + { + "left_line": 568, + "right_line": 567 + }, + { + "left_line": 569, + "right_line": 568 + }, + { + "left_line": 570, + "right_line": 569 + }, + { + "left_line": 571, + "right_line": 570 + }, + { + "left_line": 572, + "right_line": 571 + }, + { + "left_line": 573, + "right_line": 572 + }, + { + "left_line": 574, + "right_line": 573 + }, + { + "left_line": 575, + "right_line": 574 + }, + { + "left_line": 576, + "right_line": 575 + }, + { + "left_line": 577, + "right_line": 576 + }, + { + "left_line": 578, + "right_line": 577 + }, + { + "left_line": 579, + "right_line": 578 + }, + { + "left_line": 580, + "right_line": 579 + }, + { + "left_line": 581, + "right_line": 580 + }, + { + "left_line": 582, + "right_line": 581 + }, + { + "left_line": 583, + "right_line": 582 + }, + { + "left_line": 584, + "right_line": 583 + }, + { + "left_line": 585, + "right_line": 584 + }, + { + "left_line": 586, + "right_line": 585 + }, + { + "left_line": 587, + "right_line": 586 + }, + { + "left_line": 588, + "right_line": 587 + }, + { + "left_line": 589, + "right_line": 588 + }, + { + "left_line": 590, + "right_line": 589 + }, + { + "left_line": 591, + "right_line": 590 + }, + { + "left_line": 592, + "right_line": 591 + }, + { + "left_line": 593, + "right_line": 592 + }, + { + "left_line": 594, + "right_line": 593 + }, + { + "left_line": 595, + "right_line": 594 + }, + { + "left_line": 596, + "right_line": 595 + }, + { + "left_line": 597, + "right_line": 596 + }, + { + "left_line": 598, + "right_line": 597 + }, + { + "left_line": 599, + "right_line": 598 + }, + { + "left_line": 600, + "right_line": 599 + }, + { + "left_line": 601, + "right_line": 600 + }, + { + "left_line": 602, + "right_line": 601 + }, + { + "left_line": 603, + "right_line": 602 + }, + { + "left_line": 604, + "right_line": 603 + }, + { + "left_line": 605, + "right_line": 604 + }, + { + "left_line": 606, + "right_line": 605 + }, + { + "left_line": 607, + "right_line": 606 + }, + { + "left_line": 608, + "right_line": 607 + }, + { + "left_line": 609, + "right_line": 608 + }, + { + "left_line": 610, + "right_line": 609 + }, + { + "left_line": 611, + "right_line": 610 + }, + { + "left_line": 612, + "right_line": 611 + }, + { + "left_line": 613, + "right_line": 612 + }, + { + "left_line": 614, + "right_line": 613 + }, + { + "left_line": 615, + "right_line": 614 + }, + { + "left_line": 616, + "right_line": 615 + }, + { + "left_line": 617, + "right_line": 616 + }, + { + "left_line": 618, + "right_line": 617 + }, + { + "left_line": 619, + "right_line": 618 + }, + { + "left_line": 620, + "right_line": 619 + }, + { + "left_line": 621, + "right_line": 620 + }, + { + "left_line": 622, + "right_line": 621 + }, + { + "left_line": 623, + "right_line": 622 + }, + { + "left_line": 624, + "right_line": 623 + }, + { + "left_line": 625, + "right_line": 624 + }, + { + "left_line": 626, + "right_line": 625 + }, + { + "left_line": 627, + "right_line": 626 + }, + { + "left_line": 628, + "right_line": 627 + }, + { + "left_line": 629, + "right_line": 628 + }, + { + "left_line": 630, + "right_line": 629 + }, + { + "left_line": 631, + "right_line": 630 + }, + { + "left_line": 632, + "right_line": 631 + }, + { + "left_line": 633, + "right_line": 632 + }, + { + "left_line": 634, + "right_line": 633 + }, + { + "left_line": 635, + "right_line": 634 + }, + { + "left_line": 636, + "right_line": 635 + }, + { + "left_line": 637, + "right_line": 636 + }, + { + "left_line": 638, + "right_line": 637 + }, + { + "left_line": 639, + "right_line": 638 + }, + { + "left_line": 640, + "right_line": 639 + }, + { + "left_line": 641, + "right_line": 640 + }, + { + "left_line": 642, + "right_line": 641 + }, + { + "left_line": 643, + "right_line": 642 + }, + { + "left_line": 644, + "right_line": 643 + }, + { + "left_line": 645, + "right_line": 644 + }, + { + "left_line": 646, + "right_line": 645 + }, + { + "left_line": 647, + "right_line": 646 + }, + { + "left_line": 648, + "right_line": 647 + }, + { + "left_line": 649, + "right_line": 648 + }, + { + "left_line": 650, + "right_line": 649 + }, + { + "left_line": 651, + "right_line": 650 + }, + { + "left_line": 652, + "right_line": 651 + }, + { + "left_line": 653, + "right_line": 652 + }, + { + "left_line": 654, + "right_line": 653 + }, + { + "left_line": 655, + "right_line": 654 + }, + { + "left_line": 656, + "right_line": 655 + }, + { + "left_line": 657, + "right_line": 656 + }, + { + "left_line": 658, + "right_line": 657 + }, + { + "left_line": 659, + "right_line": 658 + }, + { + "left_line": 660, + "right_line": 659 + }, + { + "left_line": 661, + "right_line": 660 + }, + { + "left_line": 662, + "right_line": 661 + }, + { + "left_line": 663, + "right_line": 662 + }, + { + "left_line": 664, + "right_line": 663 + }, + { + "left_line": 665, + "right_line": 664 + }, + { + "left_line": 666, + "right_line": 665 + }, + { + "left_line": 667, + "right_line": 666 + }, + { + "left_line": 668, + "right_line": 667 + }, + { + "left_line": 669, + "right_line": 668 + }, + { + "left_line": 670, + "right_line": 669 + }, + { + "left_line": 671, + "right_line": 670 + }, + { + "left_line": 672, + "right_line": 671 + }, + { + "left_line": 673, + "right_line": 672 + }, + { + "left_line": 674, + "right_line": 673 + }, + { + "left_line": 675, + "right_line": 674 + }, + { + "left_line": 676, + "right_line": 675 + }, + { + "left_line": 677, + "right_line": 676 + }, + { + "left_line": 678, + "right_line": 677 + }, + { + "left_line": 679, + "right_line": 678 + }, + { + "left_line": 680, + "right_line": 679 + }, + { + "left_line": 681, + "right_line": 680 + }, + { + "left_line": 682, + "right_line": 681 + }, + { + "left_line": 683, + "right_line": 682 + }, + { + "left_line": 684, + "right_line": 683 + }, + { + "left_line": 685, + "right_line": 684 + }, + { + "left_line": 686, + "right_line": 685 + }, + { + "left_line": 687, + "right_line": 686 + }, + { + "left_line": 688, + "right_line": 687 + }, + { + "left_line": 689, + "right_line": 688 + }, + { + "left_line": 690, + "right_line": 689 + }, + { + "left_line": 691, + "right_line": 690 + }, + { + "left_line": 692, + "right_line": 691 + }, + { + "left_line": 693, + "right_line": 692 + }, + { + "left_line": 694, + "right_line": 693 + }, + { + "left_line": 695, + "right_line": 694 + }, + { + "left_line": 696, + "right_line": 695 + }, + { + "left_line": 697, + "right_line": 696 + }, + { + "left_line": 698, + "right_line": 697 + }, + { + "left_line": 699, + "right_line": 698 + }, + { + "left_line": 700, + "right_line": 699 + }, + { + "left_line": 701, + "right_line": 700 + }, + { + "left_line": 702, + "right_line": 701 + }, + { + "left_line": 703, + "right_line": 702 + }, + { + "left_line": 704, + "right_line": 703 + }, + { + "left_line": 705, + "right_line": 704 + }, + { + "left_line": 706, + "right_line": 705 + }, + { + "left_line": 707, + "right_line": 706 + }, + { + "left_line": 708, + "right_line": 707 + }, + { + "left_line": 709, + "right_line": 708 + }, + { + "left_line": 710, + "right_line": 709 + }, + { + "left_line": 711, + "right_line": 710 + }, + { + "left_line": 712, + "right_line": 711 + }, + { + "left_line": 713, + "right_line": 712 + }, + { + "left_line": 714, + "right_line": 713 + }, + { + "left_line": 715, + "right_line": 714 + }, + { + "left_line": 716, + "right_line": 715 + }, + { + "left_line": 717, + "right_line": 716 + }, + { + "left_line": 718, + "right_line": 717 + }, + { + "left_line": 719, + "right_line": 718 + }, + { + "left_line": 720, + "right_line": 719 + }, + { + "left_line": 721, + "right_line": 720 + }, + { + "left_line": 722, + "right_line": 721 + }, + { + "left_line": 723, + "right_line": 722 + }, + { + "left_line": 724, + "right_line": 723 + }, + { + "left_line": 725, + "right_line": 724 + }, + { + "left_line": 726, + "right_line": 725 + }, + { + "left_line": 727, + "right_line": 726 + }, + { + "left_line": 728, + "right_line": 727 + }, + { + "left_line": 729, + "right_line": 728 + }, + { + "left_line": 730, + "right_line": 729 + }, + { + "left_line": 731, + "right_line": 730 + }, + { + "left_line": 732, + "right_line": 731 + }, + { + "left_line": 733, + "right_line": 732 + }, + { + "left_line": 734, + "right_line": 733 + }, + { + "left_line": 735, + "right_line": 734 + }, + { + "left_line": 736, + "right_line": 735 + }, + { + "left_line": 737, + "right_line": 736 + }, + { + "left_line": 738, + "right_line": 737 + }, + { + "left_line": 739, + "right_line": 738 + }, + { + "left_line": 740, + "right_line": 739 + }, + { + "left_line": 741, + "right_line": 740 + }, + { + "left_line": 742, + "right_line": 741 + }, + { + "left_line": 743, + "right_line": 742 + }, + { + "left_line": 744, + "right_line": 743 + }, + { + "left_line": 745, + "right_line": 744 + }, + { + "left_line": 746, + "right_line": 745 + }, + { + "left_line": 747, + "right_line": 746 + }, + { + "left_line": 748, + "right_line": 747 + }, + { + "left_line": 749, + "right_line": 748 + }, + { + "left_line": 750, + "right_line": 749 + }, + { + "left_line": 751, + "right_line": 750 + }, + { + "left_line": 752, + "right_line": 751 + }, + { + "left_line": 753, + "right_line": 752 + }, + { + "left_line": 754, + "right_line": 753 + }, + { + "left_line": 755, + "right_line": 754 + }, + { + "left_line": 756, + "right_line": 755 + }, + { + "left_line": 757, + "right_line": 756 + }, + { + "left_line": 758, + "right_line": 757 + }, + { + "left_line": 759, + "right_line": 758 + }, + { + "left_line": 760, + "right_line": 759 + }, + { + "left_line": 761, + "right_line": 760 + }, + { + "left_line": 762, + "right_line": 761 + }, + { + "left_line": 763, + "right_line": 762 + }, + { + "left_line": 764, + "right_line": 763 + }, + { + "left_line": 765, + "right_line": 764 + }, + { + "left_line": 766, + "right_line": 765 + }, + { + "left_line": 767, + "right_line": 766 + }, + { + "left_line": 768, + "right_line": 767 + }, + { + "left_line": 769, + "right_line": 768 + }, + { + "left_line": 770, + "right_line": 769 + }, + { + "left_line": 771, + "right_line": 770 + }, + { + "left_line": 772, + "right_line": 771 + }, + { + "left_line": 773, + "right_line": 772 + }, + { + "left_line": 774, + "right_line": 773 + }, + { + "left_line": 775, + "right_line": 774 + }, + { + "left_line": 776, + "right_line": 775 + }, + { + "left_line": 777, + "right_line": 776 + }, + { + "left_line": 778, + "right_line": 777 + }, + { + "left_line": 779, + "right_line": 778 + }, + { + "left_line": 780, + "right_line": 779 + }, + { + "left_line": 781, + "right_line": 780 + }, + { + "left_line": 782, + "right_line": 781 + }, + { + "left_line": 783, + "right_line": 782 + }, + { + "left_line": 784, + "right_line": 783 + }, + { + "left_line": 785, + "right_line": 784 + }, + { + "left_line": 786, + "right_line": 785 + }, + { + "left_line": 787, + "right_line": 786 + }, + { + "left_line": 788, + "right_line": 787 + }, + { + "left_line": 789, + "right_line": 788 + }, + { + "left_line": 790, + "right_line": 789 + }, + { + "left_line": 791, + "right_line": 790 + }, + { + "left_line": 792, + "right_line": 791 + }, + { + "left_line": 793, + "right_line": 792 + }, + { + "left_line": 794, + "right_line": 793 + }, + { + "left_line": 795, + "right_line": 794 + }, + { + "left_line": 796, + "right_line": 795 + }, + { + "left_line": 797, + "right_line": 796 + }, + { + "left_line": 798, + "right_line": 797 + }, + { + "left_line": 799, + "right_line": 798 + }, + { + "left_line": 800, + "right_line": 799 + }, + { + "left_line": 801, + "right_line": 800 + }, + { + "left_line": 802, + "right_line": 801 + }, + { + "left_line": 803, + "right_line": 802 + }, + { + "left_line": 804, + "right_line": 803 + }, + { + "left_line": 805, + "right_line": 804 + }, + { + "left_line": 806, + "right_line": 805 + }, + { + "left_line": 807, + "right_line": 806 + }, + { + "left_line": 808, + "right_line": 807 + }, + { + "left_line": 809, + "right_line": 808 + }, + { + "left_line": 810, + "right_line": 809 + }, + { + "left_line": 811, + "right_line": 810 + }, + { + "left_line": 812, + "right_line": 811 + }, + { + "left_line": 813, + "right_line": 812 + }, + { + "left_line": 814, + "right_line": 813 + }, + { + "left_line": 815, + "right_line": 814 + }, + { + "left_line": 816, + "right_line": 815 + }, + { + "left_line": 817, + "right_line": 816 + }, + { + "left_line": 818, + "right_line": 817 + }, + { + "left_line": 819, + "right_line": 818 + }, + { + "left_line": 820, + "right_line": 819 + }, + { + "left_line": 821, + "right_line": 820 + }, + { + "left_line": 822, + "right_line": 821 + }, + { + "left_line": 823, + "right_line": 822 + }, + { + "left_line": 824, + "right_line": 823 + }, + { + "left_line": 825, + "right_line": 824 + }, + { + "left_line": 826, + "right_line": 825 + }, + { + "left_line": 827, + "right_line": 826 + }, + { + "left_line": 828, + "right_line": 827 + }, + { + "left_line": 829, + "right_line": 828 + }, + { + "left_line": 830, + "right_line": 829 + }, + { + "left_line": 831, + "right_line": 830 + }, + { + "left_line": 832, + "right_line": 831 + }, + { + "left_line": 833, + "right_line": 832 + }, + { + "left_line": 834, + "right_line": 833 + }, + { + "left_line": 835, + "right_line": 834 + }, + { + "left_line": 836, + "right_line": 835 + }, + { + "left_line": 837, + "right_line": 836 + }, + { + "left_line": 838, + "right_line": 837 + }, + { + "left_line": 839, + "right_line": 838 + }, + { + "left_line": 840, + "right_line": 839 + }, + { + "left_line": 841, + "right_line": 840 + }, + { + "left_line": 842, + "right_line": 841 + }, + { + "left_line": 843, + "right_line": 842 + }, + { + "left_line": 844, + "right_line": 843 + }, + { + "left_line": 845, + "right_line": 844 + }, + { + "left_line": 846, + "right_line": 845 + }, + { + "left_line": 847, + "right_line": 846 + }, + { + "left_line": 848, + "right_line": 847 + }, + { + "left_line": 849, + "right_line": 848 + }, + { + "left_line": 850, + "right_line": 849 + }, + { + "left_line": 851, + "right_line": 850 + }, + { + "left_line": 852, + "right_line": 851 + }, + { + "left_line": 853, + "right_line": 852 + }, + { + "left_line": 854, + "right_line": 853 + }, + { + "left_line": 855, + "right_line": 854 + }, + { + "left_line": 856, + "right_line": 855 + }, + { + "left_line": 857, + "right_line": 856 + }, + { + "left_line": 858, + "right_line": 857 + }, + { + "left_line": 859, + "right_line": 858 + }, + { + "left_line": 860, + "right_line": 859 + }, + { + "left_line": 861, + "right_line": 860 + }, + { + "left_line": 862, + "right_line": 861 + }, + { + "left_line": 863, + "right_line": 862 + }, + { + "left_line": 864, + "right_line": 863 + }, + { + "left_line": 865, + "right_line": 864 + }, + { + "left_line": 866, + "right_line": 865 + }, + { + "left_line": 867, + "right_line": 866 + }, + { + "left_line": 868, + "right_line": 867 + }, + { + "left_line": 869, + "right_line": 868 + }, + { + "left_line": 870, + "right_line": 869 + }, + { + "left_line": 871, + "right_line": 870 + }, + { + "left_line": 872, + "right_line": 871 + }, + { + "left_line": 873, + "right_line": 872 + }, + { + "left_line": 874, + "right_line": 873 + }, + { + "left_line": 875, + "right_line": 874 + }, + { + "left_line": 876, + "right_line": 875 + }, + { + "left_line": 877, + "right_line": 876 + }, + { + "left_line": 878, + "right_line": 877 + }, + { + "left_line": 879, + "right_line": 878 + }, + { + "left_line": 880, + "right_line": 879 + }, + { + "left_line": 881, + "right_line": 880 + }, + { + "left_line": 882, + "right_line": 881 + }, + { + "left_line": 883, + "right_line": 882 + }, + { + "left_line": 884, + "right_line": 883 + }, + { + "left_line": 885, + "right_line": 884 + }, + { + "left_line": 886, + "right_line": 885 + }, + { + "left_line": 887, + "right_line": 886 + }, + { + "left_line": 888, + "right_line": 887 + }, + { + "left_line": 889, + "right_line": 888 + }, + { + "left_line": 890, + "right_line": 889 + }, + { + "left_line": 891, + "right_line": 890 + }, + { + "left_line": 892, + "right_line": 891 + }, + { + "left_line": 893, + "right_line": 892 + }, + { + "left_line": 894, + "right_line": 893 + }, + { + "left_line": 895, + "right_line": 894 + }, + { + "left_line": 896, + "right_line": 895 + }, + { + "left_line": 897, + "right_line": 896 + }, + { + "left_line": 898, + "right_line": 897 + }, + { + "left_line": 899, + "right_line": 898 + }, + { + "left_line": 900, + "right_line": 899 + }, + { + "left_line": 901, + "right_line": 900 + }, + { + "left_line": 902, + "right_line": 901 + }, + { + "left_line": 903, + "right_line": 902 + }, + { + "left_line": 904, + "right_line": 903 + }, + { + "left_line": 905, + "right_line": 904 + }, + { + "left_line": 906, + "right_line": 905 + }, + { + "left_line": 907, + "right_line": 906 + }, + { + "left_line": 908, + "right_line": 907 + }, + { + "left_line": 909, + "right_line": 908 + }, + { + "left_line": 910, + "right_line": 909 + }, + { + "left_line": 911, + "right_line": 910 + }, + { + "left_line": 912, + "right_line": 911 + }, + { + "left_line": 913, + "right_line": 912 + }, + { + "left_line": 914, + "right_line": 913 + }, + { + "left_line": 915, + "right_line": 914 + }, + { + "left_line": 916, + "right_line": 915 + }, + { + "left_line": 917, + "right_line": 916 + }, + { + "left_line": 918, + "right_line": 917 + }, + { + "left_line": 919, + "right_line": 918 + }, + { + "left_line": 920, + "right_line": 919 + }, + { + "left_line": 921, + "right_line": 920 + }, + { + "left_line": 922, + "right_line": 921 + }, + { + "left_line": 923, + "right_line": 922 + }, + { + "left_line": 924, + "right_line": 923 + }, + { + "left_line": 925, + "right_line": 924 + }, + { + "left_line": 926, + "right_line": 925 + }, + { + "left_line": 927, + "right_line": 926 + }, + { + "left_line": 928, + "right_line": 927 + }, + { + "left_line": 929, + "right_line": 928 + }, + { + "left_line": 930, + "right_line": 929 + }, + { + "left_line": 931, + "right_line": 930 + }, + { + "left_line": 932, + "right_line": 931 + }, + { + "left_line": 933, + "right_line": 932 + }, + { + "left_line": 934, + "right_line": 933 + }, + { + "left_line": 935, + "right_line": 934 + }, + { + "left_line": 936, + "right_line": 935 + }, + { + "left_line": 937, + "right_line": 936 + }, + { + "left_line": 938, + "right_line": 937 + }, + { + "left_line": 939, + "right_line": 938 + }, + { + "left_line": 940, + "right_line": 939 + }, + { + "left_line": 941, + "right_line": 940 + }, + { + "left_line": 942, + "right_line": 941 + }, + { + "left_line": 943, + "right_line": 942 + }, + { + "left_line": 944, + "right_line": 943 + }, + { + "left_line": 945, + "right_line": 944 + }, + { + "left_line": 946, + "right_line": 945 + }, + { + "left_line": 947, + "right_line": 946 + }, + { + "left_line": 948, + "right_line": 947 + }, + { + "left_line": 949, + "right_line": 948 + }, + { + "left_line": 950, + "right_line": 949 + }, + { + "left_line": 951, + "right_line": 950 + }, + { + "left_line": 952, + "right_line": 951 + }, + { + "left_line": 953, + "right_line": 952 + }, + { + "left_line": 954, + "right_line": 953 + }, + { + "left_line": 955, + "right_line": 954 + }, + { + "left_line": 956, + "right_line": 955 + }, + { + "left_line": 957, + "right_line": 956 + }, + { + "left_line": 958, + "right_line": 957 + }, + { + "left_line": 959, + "right_line": 958 + }, + { + "left_line": 960, + "right_line": 959 + }, + { + "left_line": 961, + "right_line": 960 + }, + { + "left_line": 962, + "right_line": 961 + }, + { + "left_line": 963, + "right_line": 962 + }, + { + "left_line": 964, + "right_line": 963 + }, + { + "left_line": 965, + "right_line": 964 + }, + { + "left_line": 966, + "right_line": 965 + }, + { + "left_line": 967, + "right_line": 966 + }, + { + "left_line": 968, + "right_line": 967 + }, + { + "left_line": 969, + "right_line": 968 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_fs_api_small/metadata.json b/tests/testdata/lua_neovim_fs_api_small/metadata.json new file mode 100644 index 0000000..cb7e27a --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_small/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "a7f422fa484b41f4999e340db1c97bfc78cbee24", + "commit": "6cdab150", + "message": "docs: misc #41063", + "author": "Justin M. Keyes", + "date": "2026-07-30T15:09:19-04:00", + "language": "lua", + "files_changed": 1, + "insertions": 17, + "deletions": 18 +} diff --git a/tests/testdata/lua_neovim_fs_api_small/new.lua b/tests/testdata/lua_neovim_fs_api_small/new.lua new file mode 100644 index 0000000..1c48944 --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_small/new.lua @@ -0,0 +1,968 @@ +--- @brief +--- [vim.fs.copy()]() +--- +--- Use |filecopy()| or |uv.fs_copyfile()| to performantly copy an existing file. +--- +--- Example: +--- +--- ```lua +--- vim.fn.filecopy('foo.txt', 'bar.txt') +--- ``` +--- +--- [vim.fs.exists()]() +--- +--- Use |uv.fs_stat()| to check a file's type, and whether it exists. +--- +--- Example: +--- +--- ```lua +--- if vim.uv.fs_stat(file) then +--- vim.print('file exists') +--- end +--- ``` +--- +--- [vim.fs.read()]() +--- +--- You can use |readblob()| to get a file's contents without explicitly opening/closing it. +--- Or use |io.lines()| to iterate lines in a text file. +--- +--- Example: +--- ```lua +--- vim.print(vim.fn.readblob('.git/config')) +--- ``` +--- +--- [vim.fs.write()]() +--- +--- You can use |writefile()| to write a file without explicitly opening/closing it. +--- +--- Example: +--- ```lua +--- vim.fn.writefile('foo\0bar', 'data.bin', 'b') +--- ``` + +local uv = vim.uv + +local M = {} + +local iswin = vim.fn.has('win32') == 1 +local os_sep = iswin and '\\' or '/' + +--- Iterate over all the parents of the given path (not expanded/resolved, the caller must do that). +--- +--- Example: +--- +--- ```lua +--- local root_dir +--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do +--- if vim.fn.isdirectory(dir .. '/.git') == 1 then +--- root_dir = dir +--- break +--- end +--- end +--- +--- if root_dir then +--- print('Found git repository at', root_dir) +--- end +--- ``` +--- +---@since 10 +---@param start (string) Initial path. +---@return fun(_, dir: string): string? # Iterator +---@return nil +---@return string|nil +function M.parents(start) + return function(_, dir) + local parent = M.dirname(dir) + if parent == dir then + return nil + end + + return parent + end, + nil, + start +end + +--- Gets the parent directory of the given path (not expanded/resolved, the caller must do that). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Parent directory of `file` +function M.dirname(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local dir = vim.fn.fnamemodify(file, ':h') + if iswin then + return (dir:gsub(os_sep, '/')) + end + return dir +end + +--- Gets the basename of the given path (not expanded/resolved). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Basename of `file` +function M.basename(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local name = vim.fn.fnamemodify(file, ':t') + if iswin then + return (name:gsub(os_sep, '/')) + end + return name +end + +--- Concatenates partial paths (one absolute or relative path followed by zero or more relative +--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are +--- replaced with forward-slashes. Empty segments are removed. Paths are not expanded/resolved. +--- +--- Examples: +--- - "foo/", "/bar" => "foo/bar" +--- - "", "after/plugin" => "after/plugin" +--- - Windows: "a\foo\", "\bar" => "a/foo/bar" +--- +---@since 12 +---@param ... string +---@return string +function M.joinpath(...) + local n = select('#', ...) + ---@type string[] + local segments = {} + for i = 1, n do + local s = select(i, ...) + if s and #s > 0 then + segments[#segments + 1] = s + end + end + + local path = table.concat(segments, '/') + + return (path:gsub(iswin and '[/\\][/\\]*' or '//+', '/')) +end + +--- Wrapper around `uv.fs_scandir_next()` that ensures a file type is returned. +--- +--- @param fs uv.uv_fs_t +--- @param path string +--- @return string? +--- @return string? +local function fs_scandir_next(fs, path) + -- use uv.fs_lstat instead of uv.fs_stat to avoid descending into a symlink entry as a directory/file + local name, etype = uv.fs_scandir_next(fs) + + if not name then + return + end + + if etype == nil then + local stat = vim.uv.fs_lstat(M.joinpath(path, name)) + -- Workaround #39612 https://github.com/luvit/luv/issues/660 + etype = stat and stat.type or 'unknown' + end + + return name, etype +end + +--- @class vim.fs.dir.Opts +--- @inlinedoc +--- +--- How deep to traverse. +--- (default: `1`) +--- @field depth? integer +--- +--- Report errors via the iterator's third value ("err"), instead of silently skipping. +--- (default: `false`) +--- @field err? boolean +--- +--- Predicate to control traversal. +--- Return false to stop searching the current directory. +--- Only useful when depth > 1 +--- Return an iterator over the items located in {path} +--- @field skip? (fun(dir_name: string): boolean) +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean +--- +--- Expand "~" and "$" in {path} before scanning the directory. +--- (default: `true`) +--- @field normalize? boolean + +--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|). +--- +--- Example: +--- +--- ```lua +--- for name, type, err in vim.fs.dir(path, { err = true }) do +--- if err then +--- -- Failed to scan directory {name} (may be the root {path} itself). +--- end +--- end +--- ``` +--- +---@since 10 +---@param path (string) Directory to iterate over, normalized via |vim.fs.normalize()| unless +--- `opts.normalize=false`. +---@param opts? vim.fs.dir.Opts Optional keyword arguments: +---@return fun(): string?, string?, string? # Iterator over items in {path}, yielding (name, type, err): +--- - name: Basename of the item relative to {path}. +--- - type: One of: "file", "directory", "link", "fifo", "socket", "char", "block", "unknown". +--- - err: Error string, or nil. Only if `opts.err=true`. If the root {path} itself could not +--- be scanned, yields a single (name, nil, err) item. +function M.dir(path, opts) + opts = opts or {} + + vim.validate('path', path, 'string') + vim.validate('depth', opts.depth, 'number', true) + vim.validate('err', opts.err, 'boolean', true) + vim.validate('follow', opts.follow, 'boolean', true) + vim.validate('skip', opts.skip, 'function', true) + vim.validate('normalize', opts.normalize, 'boolean', true) + + if opts.normalize ~= false then + path = M.normalize(path) + end + + local rootfs, rooterr = uv.fs_scandir(path) + + if not rootfs then + -- Root scan failed: + -- - If opts.err=false, behave as an empty listing (back-compat). + -- - If opts.err=true, surface yield a single (name, nil, err) result. + local done = not opts.err + return function() + if done then + return nil + end + done = true + return path, nil, rooterr + end + end + + if not opts.depth or opts.depth == 1 then + return function() + return fs_scandir_next(rootfs, path) + end + end + + --- @async + return coroutine.wrap(function() + local dirs = { { path, 1, rootfs } } + while #dirs > 0 do + --- @type string, integer, any + local dir0, level, fs = unpack(table.remove(dirs, 1)) + local dir = level == 1 and dir0 or M.joinpath(path, dir0) + while fs do + local name, t = fs_scandir_next(fs, dir) + if not name then + break + end + local f = level == 1 and name or M.joinpath(dir0, name) + local err_scan = nil + if + opts.depth + and level < opts.depth + and (t == 'directory' or (t == 'link' and opts.follow and ( + uv.fs_stat(M.joinpath(path, f)) or {} + ).type == 'directory')) + and (not opts.skip or opts.skip(f) ~= false) + then + local fs_next, err = uv.fs_scandir(M.joinpath(path, f)) + if not fs_next then + err_scan = opts.err and err or nil + else + dirs[#dirs + 1] = { f, level + 1, fs_next } + end + end + coroutine.yield(f, t, err_scan) + end + end + end) +end + +--- @class vim.fs.find.Opts +--- @inlinedoc +--- +--- Path to begin searching from, defaults to |current-directory|. Not expanded. +--- @field path? string +--- +--- Search upward through parent directories. Otherwise, search child directories (recursively). +--- (default: `false`) +--- @field upward? boolean +--- +--- Stop searching when this directory is reached. The directory itself is not searched. +--- @field stop? string +--- +--- Find only items of the given type. If omitted, all items that match {names} are included. +--- @field type? string +--- +--- Stop searching after this many matches. Use `math.huge` for "unlimited". +--- (default: `1`) +--- @field limit? number +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean + +--- Find files or directories (or other items as specified by `opts.type`) in the given path. +--- +--- Finds items given in {names} starting from {path}. If {upward} is "true" +--- then the search traverses upward through parent directories; otherwise, +--- the search traverses downward. Note that downward searches are recursive +--- and may search through many directories! If {stop} is non-nil, then the +--- search stops when the directory given in {stop} is reached. The search +--- terminates when {limit} (default 1) matches are found. You can set {type} +--- to "file", "directory", "link", "socket", "char", "block", or "fifo" +--- to narrow the search to find only that type. +--- +--- Examples: +--- +--- ```lua +--- -- List all test directories under the runtime directory. +--- local dirs = vim.fs.find( +--- { 'test', 'tst', 'testdir' }, +--- { limit = math.huge, type = 'directory', path = './runtime/' } +--- ) +--- +--- -- Get all "lib/*.cpp" and "lib/*.hpp" files, using Lua patterns. +--- -- Or use `vim.glob.to_lpeg(…):match(…)` for glob/wildcard matching. +--- local files = vim.fs.find(function(name, path) +--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$') +--- end, { limit = math.huge, type = 'file' }) +--- ``` +--- +---@since 10 +---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find. +--- Must be base names, paths and globs are not supported when {names} is a string or a table. +--- If {names} is a function, it is called for each traversed item with args: +--- - name: base name of the current item +--- - path: full path of the current item +--- +--- The function should return `true` if the given item is considered a match. +--- +---@param opts? vim.fs.find.Opts Optional keyword arguments: +---@return string[] # Normalized paths |vim.fs.normalize()| of all matching items. +---@return string[] # Errors collected while searching. +function M.find(names, opts) + opts = opts or {} + vim.validate('names', names, { 'string', 'table', 'function' }) + vim.validate('path', opts.path, 'string', true) + vim.validate('upward', opts.upward, 'boolean', true) + vim.validate('stop', opts.stop, 'string', true) + vim.validate('type', opts.type, 'string', true) + vim.validate('limit', opts.limit, 'number', true) + vim.validate('follow', opts.follow, 'boolean', true) + + if type(names) == 'string' then + names = { names } + end + + local path = opts.path or assert(uv.cwd()) + local stop = opts.stop + local limit = opts.limit or 1 + + local matches = {} --- @type string[] + local errors = {} --- @type string[] + + local function add(match) + matches[#matches + 1] = M.normalize(match) + if #matches == limit then + return true + end + end + + if opts.upward then + local test --- @type fun(p: string): string[] + + if type(names) == 'function' then + test = function(p) + local t = {} + for name, type, err in M.dir(p, { err = true }) do + if err ~= nil then + table.insert(errors, err) + elseif (not opts.type or opts.type == type) and names(name, p) then + table.insert(t, M.joinpath(p, name)) + end + end + return t + end + else + test = function(p) + local t = {} --- @type string[] + local ok, aerr = uv.fs_access(p, 'R') -- Check if the root dir is readable. + if not ok then + table.insert(errors, aerr) + return t + end + for _, name in ipairs(names) do + local f = M.joinpath(p, name) + local stat = uv.fs_stat(f) + if stat and (not opts.type or opts.type == stat.type) then + t[#t + 1] = f + end + end + + return t + end + end + + for _, match in ipairs(test(path)) do + if add(match) then + return matches, errors + end + end + + for parent in M.parents(path) do + if stop and parent == stop then + break + end + + for _, match in ipairs(test(parent)) do + if add(match) then + return matches, errors + end + end + end + else + local dirs = { path } + while #dirs > 0 do + local dir = table.remove(dirs, 1) + if stop and dir == stop then + break + end + + for other, type_, err in M.dir(dir, { err = true }) do + if err ~= nil then + table.insert(errors, err) + else + local f = M.joinpath(dir, other) + if type(names) == 'function' then + if (not opts.type or opts.type == type_) and names(other, dir) then + if add(f) then + return matches, errors + end + end + else + for _, name in ipairs(names) do + if name == other and (not opts.type or opts.type == type_) then + if add(f) then + return matches, errors + end + end + end + end + + if + type_ == 'directory' + or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory') + then + dirs[#dirs + 1] = f + end + end + end + end + end + + return matches, errors +end + +--- Find the first parent directory containing a specific "marker", relative to a file path or +--- buffer. +--- +--- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search +--- begins from Nvim's |current-directory|. +--- +--- Examples: +--- +--- ```lua +--- -- Find the root of a Python project, starting from file 'main.py' +--- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' }) +--- +--- -- Find the root of a git repository +--- vim.fs.root(0, '.git') +--- +--- -- Find the parent directory containing any file with a .csproj extension +--- vim.fs.root(0, function(name, path) +--- return vim.fs.ext(name) == 'csproj' +--- end) +--- +--- -- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if +--- -- not found, find the first ancestor containing ".git": +--- vim.fs.root(0, { { 'stylua.toml', '.luarc.json' }, '.git' }) +--- ``` +--- +--- @since 12 +--- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or +--- relative, expanded via `abspath()`) to begin the search from. +--- @param marker (string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean +--- Filename, function, or list thereof, that decides how to find the root. To +--- indicate "equal priority", specify items in a nested list `{ { 'a.txt', 'b.lua' }, … }`. +--- A function item must return true if `name` and `path` are a match. Each item +--- (which may itself be a nested list) is evaluated in-order against all ancestors, +--- until a match is found. +--- @return string? # Directory path containing one of the given markers, or nil if no directory was +--- found. +function M.root(source, marker) + assert(source, 'missing required argument: source') + assert(marker, 'missing required argument: marker') + + local path ---@type string + if type(source) == 'string' then + path = source + elseif type(source) == 'number' then + if vim.bo[source].buftype ~= '' then + path = assert(uv.cwd()) + else + path = vim.api.nvim_buf_get_name(source) + end + else + error('invalid type for argument "source": expected string or buffer number') + end + + local markers = type(marker) == 'table' and marker or { marker } + for _, mark in ipairs(markers) do + local paths = M.find(mark, { + upward = true, + path = M.abspath(path), + }) + + if #paths ~= 0 then + local dir = M.dirname(paths[1]) + return dir and M.abspath(dir) or nil + end + end + + return nil +end + +--- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX +--- path. The path must use forward slashes as path separator. +--- +--- Does not check if the path is a valid Windows path. Invalid paths will give invalid results. +--- +--- Examples: +--- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar` +--- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar` +--- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar` +--- - `C:/foo/bar` -> `C:`, `/foo/bar` +--- - `C:foo/bar` -> `C:`, `foo/bar` +--- +--- @param path string Path to split. +--- @return string, string, boolean : prefix, body, whether path is invalid. +local function split_windows_path(path) + local prefix = '' + + --- Match pattern. If there is a match, move the matched pattern from the path to the prefix. + --- Returns the matched pattern. + --- + --- @param pattern string Pattern to match. + --- @return string|nil Matched pattern + local function match_to_prefix(pattern) + local match = path:match(pattern) + + if match then + prefix = prefix .. match --[[ @as string ]] + path = path:sub(#match + 1) + end + + return match + end + + local function process_unc_path() + return match_to_prefix('[^/]+/+[^/]+/+') + end + + if match_to_prefix('^//[?.]/') then + -- Device paths + local device = match_to_prefix('[^/]+/+') + + -- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path + if not device or (device:match('^UNC/+$') and not process_unc_path()) then + return prefix, path, false + end + elseif match_to_prefix('^//') then + -- Process UNC path, return early if it's invalid + if not process_unc_path() then + return prefix, path, false + end + elseif path:match('^%w:') then + -- Drive paths + prefix, path = path:sub(1, 2), path:sub(3) + end + + -- If there are slashes at the end of the prefix, move them to the start of the body. This is to + -- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no + -- slashes at the end of the prefix, so it will be treated as a relative path, as it should be. + local trailing_slash = prefix:match('/+$') + + if trailing_slash then + prefix = prefix:sub(1, -1 - #trailing_slash) + path = trailing_slash .. path --[[ @as string ]] + end + + return prefix, path, true +end + +--- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes. +--- `..` is not resolved if the path is relative and resolving it requires the path to be absolute. +--- If a relative path resolves to the current directory, an empty string is returned. +--- +--- @see M.normalize() +--- @param path string Path to resolve. +--- @return string Resolved path. +local function path_resolve_dot(path) + local is_path_absolute = vim.startswith(path, '/') + local new_path_components = {} + + for component in vim.gsplit(path, '/') do + if component == '.' or component == '' then -- luacheck: ignore 542 + -- Skip `.` components and empty components + elseif component == '..' then + if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then + -- For `..`, remove the last component if we're still inside the current directory, except + -- when the last component is `..` itself + table.remove(new_path_components) + elseif is_path_absolute then -- luacheck: ignore 542 + -- Reached the root directory in absolute path, do nothing + else + -- Reached current directory in relative path, add `..` to the path + table.insert(new_path_components, component) + end + else + table.insert(new_path_components, component) + end + end + + return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/') +end + +--- Expand tilde (~) character at the beginning of the path to the user's home directory. +--- +--- @param path string Path to expand. +--- @param sep string|nil Path separator to use. Uses os_sep by default. +--- @return string Expanded path. +local function expand_home(path, sep) + sep = sep or os_sep + + if vim.startswith(path, '~') then + local home = uv.os_homedir() or '~' --- @type string + + if home:sub(-1) == sep then + home = home:sub(1, -2) + end + + path = home .. path:sub(2) --- @type string + end + + return path +end + +--- @class vim.fs.normalize.Opts +--- @inlinedoc +--- +--- Expand environment variables. +--- (default: `true`) +--- @field expand_env? boolean +--- +--- @field package _fast? boolean +--- +--- Path is a Windows path. +--- (default: `true` in Windows, `false` otherwise) +--- @field win? boolean + +--- Normalize a path to a standard format. Expands environment variables, and tilde "~" at the +--- beginning of the path. Resolves "." and ".." components, except when the path is relative and +--- resolving it would produce an absolute path. +--- +--- - "." as the only part in a relative path: +--- - "." => "." +--- - "././" => "." +--- - ".." when it leads outside the current directory +--- - "foo/../../bar" => "../bar" +--- - "../../foo" => "../../foo" +--- - ".." in the root directory returns the root directory. +--- - "/../../" => "/" +--- +--- On Windows, backslashes (`\`) are converted to forward slashes (`/`). +--- +--- Examples: +--- ```lua +--- [[C:\Users\jdoe]] --> "C:/Users/jdoe" +--- "~/src/neovim" --> "/home/jdoe/src/neovim" +--- "$XDG_CONFIG_HOME/nvim/init.vim" --> "/Users/jdoe/.config/nvim/init.vim" +--- "~/src/nvim/api/../tui/./tui.c" --> "/home/jdoe/src/nvim/tui/tui.c" +--- "./foo/bar" --> "foo/bar" +--- "foo/../../../bar" --> "../../bar" +--- "/home/jdoe/../../../bar" --> "/bar" +--- "C:foo/../../baz" --> "C:../baz" +--- "C:/foo/../../baz" --> "C:/baz" +--- [[\\?\UNC\server\share\foo\..\..\..\bar]] --> "//?/UNC/server/share/bar" +--- ``` +--- +---@since 10 +---@param path (string) Path to normalize +---@param opts? vim.fs.normalize.Opts +---@return (string) : Normalized path +function M.normalize(path, opts) + opts = opts or {} + + if not opts._fast then + vim.validate('path', path, 'string') + vim.validate('expand_env', opts.expand_env, 'boolean', true) + vim.validate('win', opts.win, 'boolean', true) + end + + local win = opts.win == nil and iswin or not not opts.win + local os_sep_local = win and '\\' or '/' + + -- Empty path is already normalized + if path == '' then + return '' + end + + -- Expand ~ to user's home directory + path = expand_home(path, os_sep_local) + + -- Expand environment variables if `opts.expand_env` isn't `false` + if opts.expand_env == nil or opts.expand_env then + path = path:gsub('%$([%w_]+)', uv.os_getenv) --- @type string + end + + if win then + -- Convert path separator to `/` + path = path:gsub(os_sep_local, '/') + end + + -- Check for double slashes at the start of the path because they have special meaning + local double_slash = false + if not opts._fast then + double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///') + end + + local prefix = '' + + if win then + local is_valid --- @type boolean + -- Split Windows paths into prefix and body to make processing easier + prefix, path, is_valid = split_windows_path(path) + + -- If path is not valid, return it as-is + if not is_valid then + return prefix .. path + end + + -- Ensure capital drive and remove extraneous slashes from the prefix + prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/') + end + + if not opts._fast then + -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix + -- and path. + path = path_resolve_dot(path) + end + + -- Preserve leading double slashes as they indicate UNC paths and DOS device paths in + -- Windows and have implementation-defined behavior in POSIX. + path = (double_slash and '/' or '') .. prefix .. path + + -- Change empty path to `.` + if path == '' then + path = '.' + end + + return path +end + +--- @class vim.fs.mkdir.Opts +--- @inlinedoc +--- +--- Create intermediate directories as necessary. +--- (default: `false`) +--- @field parents? boolean +--- +--- Permission bits for newly-created directories. +--- (default: `493`) +--- @field mode? integer + +--- Creates a directory. +--- +---@since 15 +---@param path string Path to create (not expanded/resolved). +---@param opts? vim.fs.mkdir.Opts Optional keyword arguments. +function M.mkdir(path, opts) + vim.validate('path', path, 'string') + vim.validate('opts', opts, 'table', true) + opts = opts or {} + vim.validate('parents', opts.parents, 'boolean', true) + vim.validate('mode', opts.mode, 'number', true) + + vim.fn.mkdir(path, opts.parents and 'p' or '', tostring(opts.mode or 493)) +end + +--- @param path string Path to remove +--- @param ty string type of path +--- @param recursive? boolean +--- @param force? boolean +local function rm(path, ty, recursive, force) + --- @diagnostic disable-next-line:no-unknown + local rm_fn + + if ty == 'directory' then + if recursive then + for file, fty in vim.fs.dir(path) do + rm(M.joinpath(path, file), fty, true, force) + end + elseif not force then + error(string.format('%s is a directory', path)) + end + + rm_fn = uv.fs_rmdir + else + rm_fn = uv.fs_unlink + end + + local ret, err, errnm = rm_fn(path) + if ret == nil and (not force or errnm ~= 'ENOENT') then + error(err) + end +end + +--- @class vim.fs.rm.Opts +--- @inlinedoc +--- +--- Remove directory contents recursively. +--- @field recursive? boolean +--- +--- Ignore nonexistent files and arguments. +--- @field force? boolean + +--- Removes a file or directory. +--- +--- Removes symlinks without touching the origin. To remove the origin, resolve it explicitly +--- with |uv.fs_realpath()|: +--- ```lua +--- vim.fs.rm(vim.uv.fs_realpath('symlink-dir'), { recursive = true }) +--- ``` +--- +--- @since 13 +--- @param path string Path to remove (not expanded/resolved). +--- @param opts? vim.fs.rm.Opts +function M.rm(path, opts) + opts = opts or {} + + local stat, err, errnm = uv.fs_lstat(path) + if stat then + rm(path, stat.type, opts.recursive, opts.force) + elseif not opts.force or errnm ~= 'ENOENT' then + error(err) + end +end + +--- Converts `path` to an absolute path. Expands tilde (~) at the beginning of the path +--- to the user's home directory. Does not check if the path exists, normalize the path, resolve +--- symlinks or hardlinks (including `.` and `..`), or expand environment variables. If the path is +--- already absolute, it is returned unchanged. Also converts `\` path separators to `/`. +--- +--- @since 13 +--- @param path string Path +--- @return string Absolute path +function M.abspath(path) + -- TODO(justinmk): mark f_fnamemodify as API_FAST and use it, ":p:h" should be safe... + + vim.validate('path', path, 'string') + + -- Expand ~ to user's home directory + path = expand_home(path) + + -- Convert path separator to `/` + path = path:gsub(os_sep, '/') + + local prefix = '' + + if iswin then + prefix, path = split_windows_path(path) + end + + if prefix == '//' or vim.startswith(path, '/') then + -- Path is already absolute, do nothing + return prefix .. path + end + + -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory + -- of the drive specified in the path + local cwd = assert((iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or uv.cwd()) + -- Convert cwd path separator to `/` + cwd = cwd:gsub(os_sep, '/') + + if path == '.' then + return cwd + end + -- Prefix is not needed for expanding relative paths, `cwd` already contains it. + return M.joinpath(cwd, path) +end + +--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor. +--- +--- Example: +--- +--- ```lua +--- vim.fs.relpath('/var', '/var/lib') -- 'lib' +--- vim.fs.relpath('/var', '/usr/bin') -- nil +--- ``` +--- +--- @since 13 +--- @param base string +--- @param target string +--- @param opts table? Reserved for future use +--- @return string|nil +function M.relpath(base, target, opts) + vim.validate('base', base, 'string') + vim.validate('target', target, 'string') + vim.validate('opts', opts, 'table', true) + + base = M.normalize(M.abspath(base)) + target = M.normalize(M.abspath(target)) + if base == target then + return '.' + end + + local prefix = '' + if iswin then + prefix, base = split_windows_path(base) + end + base = prefix .. base .. (base ~= '/' and '/' or '') + + return vim.startswith(target, base) and target:sub(#base + 1) or nil +end + +--- Return the file's last extension, if any. +--- +--- Similar to |fnamemodify()| with the |::e| modifier. The extension does not include a leading +--- period. +--- +--- Examples: +--- +--- ```lua +--- vim.fs.ext('archive.tar.gz') -- 'gz' +--- vim.fs.ext('~/.git') -- '' +--- vim.fs.ext('plugin/myplug.lua') -- 'lua' +--- ``` +--- +---@since 14 +---@param file string Path +---@param opts table? Reserved for future use +---@return string Extension of {file} +function M.ext(file, opts) + vim.validate('file', file, 'string') + vim.validate('opts', opts, 'table', true) + return vim.fn.fnamemodify(file, ':e') +end + +return M diff --git a/tests/testdata/lua_neovim_fs_api_small/old.lua b/tests/testdata/lua_neovim_fs_api_small/old.lua new file mode 100644 index 0000000..57e0da4 --- /dev/null +++ b/tests/testdata/lua_neovim_fs_api_small/old.lua @@ -0,0 +1,969 @@ +--- @brief +--- [vim.fs.copy()]() +--- +--- Use |filecopy()| or |uv.fs_copyfile()| to performantly copy an existing file. +--- +--- Example: +--- +--- ```lua +--- vim.fn.filecopy('foo.txt', 'bar.txt') +--- ``` +--- +--- [vim.fs.exists()]() +--- +--- Use |uv.fs_stat()| to check a file's type, and whether it exists. +--- +--- Example: +--- +--- ```lua +--- if vim.uv.fs_stat(file) then +--- vim.print('file exists') +--- end +--- ``` +--- +--- [vim.fs.read()]() +--- +--- You can use |readblob()| to get a file's contents without explicitly opening/closing it. +--- Or use |io.lines()| to iterate lines in a text file. +--- +--- Example: +--- ```lua +--- vim.print(vim.fn.readblob('.git/config')) +--- ``` +--- +--- [vim.fs.write()]() +--- +--- You can use |writefile()| to write a file without explicitly opening/closing it. +--- +--- Example: +--- ```lua +--- vim.fn.writefile('foo\0bar', 'data.bin', 'b') +--- ``` + +local uv = vim.uv + +local M = {} + +local iswin = vim.fn.has('win32') == 1 +local os_sep = iswin and '\\' or '/' + +--- Iterate over all the parents of the given path (not expanded/resolved, the caller must do that). +--- +--- Example: +--- +--- ```lua +--- local root_dir +--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do +--- if vim.fn.isdirectory(dir .. '/.git') == 1 then +--- root_dir = dir +--- break +--- end +--- end +--- +--- if root_dir then +--- print('Found git repository at', root_dir) +--- end +--- ``` +--- +---@since 10 +---@param start (string) Initial path. +---@return fun(_, dir: string): string? # Iterator +---@return nil +---@return string|nil +function M.parents(start) + return function(_, dir) + local parent = M.dirname(dir) + if parent == dir then + return nil + end + + return parent + end, + nil, + start +end + +--- Gets the parent directory of the given path (not expanded/resolved, the caller must do that). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Parent directory of `file` +function M.dirname(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local dir = vim.fn.fnamemodify(file, ':h') + if iswin then + return (dir:gsub(os_sep, '/')) + end + return dir +end + +--- Gets the basename of the given path (not expanded/resolved). +--- +---@since 10 +---@generic T : string|nil +---@param file T Path +---@return T # Basename of `file` +function M.basename(file) + if file == nil then + return nil + end + vim.validate('file', file, 'string') + local name = vim.fn.fnamemodify(file, ':t') + if iswin then + return (name:gsub(os_sep, '/')) + end + return name +end + +--- Concatenates partial paths (one absolute or relative path followed by zero or more relative +--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are +--- replaced with forward-slashes. Empty segments are removed. Paths are not expanded/resolved. +--- +--- Examples: +--- - "foo/", "/bar" => "foo/bar" +--- - "", "after/plugin" => "after/plugin" +--- - Windows: "a\foo\", "\bar" => "a/foo/bar" +--- +---@since 12 +---@param ... string +---@return string +function M.joinpath(...) + local n = select('#', ...) + ---@type string[] + local segments = {} + for i = 1, n do + local s = select(i, ...) + if s and #s > 0 then + segments[#segments + 1] = s + end + end + + local path = table.concat(segments, '/') + + return (path:gsub(iswin and '[/\\][/\\]*' or '//+', '/')) +end + +--- Wrapper around `uv.fs_scandir_next()` that ensures a file type is returned. +--- +--- @param fs uv.uv_fs_t +--- @param path string +--- @return string? +--- @return string? +local function fs_scandir_next(fs, path) + -- use uv.fs_lstat instead of uv.fs_stat to avoid descending into a symlink entry as a directory/file + local name, etype = uv.fs_scandir_next(fs) + + if not name then + return + end + + if etype == nil then + local stat = vim.uv.fs_lstat(M.joinpath(path, name)) + -- Workaround #39612 https://github.com/luvit/luv/issues/660 + etype = stat and stat.type or 'unknown' + end + + return name, etype +end + +--- @class vim.fs.dir.Opts +--- @inlinedoc +--- +--- How deep to traverse. +--- (default: `1`) +--- @field depth? integer +--- +--- Report errors via the iterator's third value ("err"), instead of silently skipping. +--- (default: `false`) +--- @field err? boolean +--- +--- Predicate to control traversal. +--- Return false to stop searching the current directory. +--- Only useful when depth > 1 +--- Return an iterator over the items located in {path} +--- @field skip? (fun(dir_name: string): boolean) +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean +--- +--- Normalize {path} via |vim.fs.normalize()|. Set `false` to use {path} literally, e.g. to list a +--- directory whose name contains `~` or `$`. +--- (default: `true`) +--- @field normalize? boolean + +--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|). +--- +--- Example: +--- +--- ```lua +--- for name, type, err in vim.fs.dir(path, { err = true }) do +--- if err then +--- -- Failed to scan directory {name} (may be the root {path} itself). +--- end +--- end +--- ``` +--- +---@since 10 +---@param path (string) Directory to iterate over, normalized via |vim.fs.normalize()| unless +--- `opts.normalize=false`. +---@param opts? vim.fs.dir.Opts Optional keyword arguments: +---@return fun(): string?, string?, string? # Iterator over items in {path}, yielding (name, type, err): +--- - name: Basename of the item relative to {path}. +--- - type: One of: "file", "directory", "link", "fifo", "socket", "char", "block", "unknown". +--- - err: Error string, or nil. Only if `opts.err=true`. If the root {path} itself could not +--- be scanned, yields a single (name, nil, err) item. +function M.dir(path, opts) + opts = opts or {} + + vim.validate('path', path, 'string') + vim.validate('depth', opts.depth, 'number', true) + vim.validate('err', opts.err, 'boolean', true) + vim.validate('follow', opts.follow, 'boolean', true) + vim.validate('skip', opts.skip, 'function', true) + vim.validate('normalize', opts.normalize, 'boolean', true) + + if opts.normalize ~= false then + path = M.normalize(path) + end + + local rootfs, rooterr = uv.fs_scandir(path) + + if not rootfs then + -- Root scan failed: + -- - If opts.err=false, behave as an empty listing (back-compat). + -- - If opts.err=true, surface yield a single (name, nil, err) result. + local done = not opts.err + return function() + if done then + return nil + end + done = true + return path, nil, rooterr + end + end + + if not opts.depth or opts.depth == 1 then + return function() + return fs_scandir_next(rootfs, path) + end + end + + --- @async + return coroutine.wrap(function() + local dirs = { { path, 1, rootfs } } + while #dirs > 0 do + --- @type string, integer, any + local dir0, level, fs = unpack(table.remove(dirs, 1)) + local dir = level == 1 and dir0 or M.joinpath(path, dir0) + while fs do + local name, t = fs_scandir_next(fs, dir) + if not name then + break + end + local f = level == 1 and name or M.joinpath(dir0, name) + local err_scan = nil + if + opts.depth + and level < opts.depth + and (t == 'directory' or (t == 'link' and opts.follow and ( + uv.fs_stat(M.joinpath(path, f)) or {} + ).type == 'directory')) + and (not opts.skip or opts.skip(f) ~= false) + then + local fs_next, err = uv.fs_scandir(M.joinpath(path, f)) + if not fs_next then + err_scan = opts.err and err or nil + else + dirs[#dirs + 1] = { f, level + 1, fs_next } + end + end + coroutine.yield(f, t, err_scan) + end + end + end) +end + +--- @class vim.fs.find.Opts +--- @inlinedoc +--- +--- Path to begin searching from, defaults to |current-directory|. Not expanded. +--- @field path? string +--- +--- Search upward through parent directories. Otherwise, search child directories (recursively). +--- (default: `false`) +--- @field upward? boolean +--- +--- Stop searching when this directory is reached. The directory itself is not searched. +--- @field stop? string +--- +--- Find only items of the given type. If omitted, all items that match {names} are included. +--- @field type? string +--- +--- Stop searching after this many matches. Use `math.huge` for "unlimited". +--- (default: `1`) +--- @field limit? number +--- +--- Follow symbolic links. +--- (default: `false`) +--- @field follow? boolean + +--- Find files or directories (or other items as specified by `opts.type`) in the given path. +--- +--- Finds items given in {names} starting from {path}. If {upward} is "true" +--- then the search traverses upward through parent directories; otherwise, +--- the search traverses downward. Note that downward searches are recursive +--- and may search through many directories! If {stop} is non-nil, then the +--- search stops when the directory given in {stop} is reached. The search +--- terminates when {limit} (default 1) matches are found. You can set {type} +--- to "file", "directory", "link", "socket", "char", "block", or "fifo" +--- to narrow the search to find only that type. +--- +--- Examples: +--- +--- ```lua +--- -- List all test directories under the runtime directory. +--- local dirs = vim.fs.find( +--- { 'test', 'tst', 'testdir' }, +--- { limit = math.huge, type = 'directory', path = './runtime/' } +--- ) +--- +--- -- Get all "lib/*.cpp" and "lib/*.hpp" files, using Lua patterns. +--- -- Or use `vim.glob.to_lpeg(…):match(…)` for glob/wildcard matching. +--- local files = vim.fs.find(function(name, path) +--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$') +--- end, { limit = math.huge, type = 'file' }) +--- ``` +--- +---@since 10 +---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find. +--- Must be base names, paths and globs are not supported when {names} is a string or a table. +--- If {names} is a function, it is called for each traversed item with args: +--- - name: base name of the current item +--- - path: full path of the current item +--- +--- The function should return `true` if the given item is considered a match. +--- +---@param opts? vim.fs.find.Opts Optional keyword arguments: +---@return string[] # Normalized paths |vim.fs.normalize()| of all matching items. +---@return string[] # Errors collected while searching. +function M.find(names, opts) + opts = opts or {} + vim.validate('names', names, { 'string', 'table', 'function' }) + vim.validate('path', opts.path, 'string', true) + vim.validate('upward', opts.upward, 'boolean', true) + vim.validate('stop', opts.stop, 'string', true) + vim.validate('type', opts.type, 'string', true) + vim.validate('limit', opts.limit, 'number', true) + vim.validate('follow', opts.follow, 'boolean', true) + + if type(names) == 'string' then + names = { names } + end + + local path = opts.path or assert(uv.cwd()) + local stop = opts.stop + local limit = opts.limit or 1 + + local matches = {} --- @type string[] + local errors = {} --- @type string[] + + local function add(match) + matches[#matches + 1] = M.normalize(match) + if #matches == limit then + return true + end + end + + if opts.upward then + local test --- @type fun(p: string): string[] + + if type(names) == 'function' then + test = function(p) + local t = {} + for name, type, err in M.dir(p, { err = true }) do + if err ~= nil then + table.insert(errors, err) + elseif (not opts.type or opts.type == type) and names(name, p) then + table.insert(t, M.joinpath(p, name)) + end + end + return t + end + else + test = function(p) + local t = {} --- @type string[] + local ok, aerr = uv.fs_access(p, 'R') -- Check if the root dir is readable. + if not ok then + table.insert(errors, aerr) + return t + end + for _, name in ipairs(names) do + local f = M.joinpath(p, name) + local stat = uv.fs_stat(f) + if stat and (not opts.type or opts.type == stat.type) then + t[#t + 1] = f + end + end + + return t + end + end + + for _, match in ipairs(test(path)) do + if add(match) then + return matches, errors + end + end + + for parent in M.parents(path) do + if stop and parent == stop then + break + end + + for _, match in ipairs(test(parent)) do + if add(match) then + return matches, errors + end + end + end + else + local dirs = { path } + while #dirs > 0 do + local dir = table.remove(dirs, 1) + if stop and dir == stop then + break + end + + for other, type_, err in M.dir(dir, { err = true }) do + if err ~= nil then + table.insert(errors, err) + else + local f = M.joinpath(dir, other) + if type(names) == 'function' then + if (not opts.type or opts.type == type_) and names(other, dir) then + if add(f) then + return matches, errors + end + end + else + for _, name in ipairs(names) do + if name == other and (not opts.type or opts.type == type_) then + if add(f) then + return matches, errors + end + end + end + end + + if + type_ == 'directory' + or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory') + then + dirs[#dirs + 1] = f + end + end + end + end + end + + return matches, errors +end + +--- Find the first parent directory containing a specific "marker", relative to a file path or +--- buffer. +--- +--- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search +--- begins from Nvim's |current-directory|. +--- +--- Examples: +--- +--- ```lua +--- -- Find the root of a Python project, starting from file 'main.py' +--- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' }) +--- +--- -- Find the root of a git repository +--- vim.fs.root(0, '.git') +--- +--- -- Find the parent directory containing any file with a .csproj extension +--- vim.fs.root(0, function(name, path) +--- return vim.fs.ext(name) == 'csproj' +--- end) +--- +--- -- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if +--- -- not found, find the first ancestor containing ".git": +--- vim.fs.root(0, { { 'stylua.toml', '.luarc.json' }, '.git' }) +--- ``` +--- +--- @since 12 +--- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or +--- relative, expanded via `abspath()`) to begin the search from. +--- @param marker (string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean +--- Filename, function, or list thereof, that decides how to find the root. To +--- indicate "equal priority", specify items in a nested list `{ { 'a.txt', 'b.lua' }, … }`. +--- A function item must return true if `name` and `path` are a match. Each item +--- (which may itself be a nested list) is evaluated in-order against all ancestors, +--- until a match is found. +--- @return string? # Directory path containing one of the given markers, or nil if no directory was +--- found. +function M.root(source, marker) + assert(source, 'missing required argument: source') + assert(marker, 'missing required argument: marker') + + local path ---@type string + if type(source) == 'string' then + path = source + elseif type(source) == 'number' then + if vim.bo[source].buftype ~= '' then + path = assert(uv.cwd()) + else + path = vim.api.nvim_buf_get_name(source) + end + else + error('invalid type for argument "source": expected string or buffer number') + end + + local markers = type(marker) == 'table' and marker or { marker } + for _, mark in ipairs(markers) do + local paths = M.find(mark, { + upward = true, + path = M.abspath(path), + }) + + if #paths ~= 0 then + local dir = M.dirname(paths[1]) + return dir and M.abspath(dir) or nil + end + end + + return nil +end + +--- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX +--- path. The path must use forward slashes as path separator. +--- +--- Does not check if the path is a valid Windows path. Invalid paths will give invalid results. +--- +--- Examples: +--- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar` +--- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar` +--- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar` +--- - `C:/foo/bar` -> `C:`, `/foo/bar` +--- - `C:foo/bar` -> `C:`, `foo/bar` +--- +--- @param path string Path to split. +--- @return string, string, boolean : prefix, body, whether path is invalid. +local function split_windows_path(path) + local prefix = '' + + --- Match pattern. If there is a match, move the matched pattern from the path to the prefix. + --- Returns the matched pattern. + --- + --- @param pattern string Pattern to match. + --- @return string|nil Matched pattern + local function match_to_prefix(pattern) + local match = path:match(pattern) + + if match then + prefix = prefix .. match --[[ @as string ]] + path = path:sub(#match + 1) + end + + return match + end + + local function process_unc_path() + return match_to_prefix('[^/]+/+[^/]+/+') + end + + if match_to_prefix('^//[?.]/') then + -- Device paths + local device = match_to_prefix('[^/]+/+') + + -- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path + if not device or (device:match('^UNC/+$') and not process_unc_path()) then + return prefix, path, false + end + elseif match_to_prefix('^//') then + -- Process UNC path, return early if it's invalid + if not process_unc_path() then + return prefix, path, false + end + elseif path:match('^%w:') then + -- Drive paths + prefix, path = path:sub(1, 2), path:sub(3) + end + + -- If there are slashes at the end of the prefix, move them to the start of the body. This is to + -- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no + -- slashes at the end of the prefix, so it will be treated as a relative path, as it should be. + local trailing_slash = prefix:match('/+$') + + if trailing_slash then + prefix = prefix:sub(1, -1 - #trailing_slash) + path = trailing_slash .. path --[[ @as string ]] + end + + return prefix, path, true +end + +--- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes. +--- `..` is not resolved if the path is relative and resolving it requires the path to be absolute. +--- If a relative path resolves to the current directory, an empty string is returned. +--- +--- @see M.normalize() +--- @param path string Path to resolve. +--- @return string Resolved path. +local function path_resolve_dot(path) + local is_path_absolute = vim.startswith(path, '/') + local new_path_components = {} + + for component in vim.gsplit(path, '/') do + if component == '.' or component == '' then -- luacheck: ignore 542 + -- Skip `.` components and empty components + elseif component == '..' then + if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then + -- For `..`, remove the last component if we're still inside the current directory, except + -- when the last component is `..` itself + table.remove(new_path_components) + elseif is_path_absolute then -- luacheck: ignore 542 + -- Reached the root directory in absolute path, do nothing + else + -- Reached current directory in relative path, add `..` to the path + table.insert(new_path_components, component) + end + else + table.insert(new_path_components, component) + end + end + + return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/') +end + +--- Expand tilde (~) character at the beginning of the path to the user's home directory. +--- +--- @param path string Path to expand. +--- @param sep string|nil Path separator to use. Uses os_sep by default. +--- @return string Expanded path. +local function expand_home(path, sep) + sep = sep or os_sep + + if vim.startswith(path, '~') then + local home = uv.os_homedir() or '~' --- @type string + + if home:sub(-1) == sep then + home = home:sub(1, -2) + end + + path = home .. path:sub(2) --- @type string + end + + return path +end + +--- @class vim.fs.normalize.Opts +--- @inlinedoc +--- +--- Expand environment variables. +--- (default: `true`) +--- @field expand_env? boolean +--- +--- @field package _fast? boolean +--- +--- Path is a Windows path. +--- (default: `true` in Windows, `false` otherwise) +--- @field win? boolean + +--- Normalize a path to a standard format. A tilde (~) character at the beginning of the path is +--- expanded to the user's home directory and environment variables are also expanded. "." and ".." +--- components are also resolved, except when the path is relative and trying to resolve it would +--- result in an absolute path. +--- - "." as the only part in a relative path: +--- - "." => "." +--- - "././" => "." +--- - ".." when it leads outside the current directory +--- - "foo/../../bar" => "../bar" +--- - "../../foo" => "../../foo" +--- - ".." in the root directory returns the root directory. +--- - "/../../" => "/" +--- +--- On Windows, backslash (\) characters are converted to forward slashes (/). +--- +--- Examples: +--- ``` +--- [[C:\Users\jdoe]] => "C:/Users/jdoe" +--- "~/src/neovim" => "/home/jdoe/src/neovim" +--- "$XDG_CONFIG_HOME/nvim/init.vim" => "/Users/jdoe/.config/nvim/init.vim" +--- "~/src/nvim/api/../tui/./tui.c" => "/home/jdoe/src/nvim/tui/tui.c" +--- "./foo/bar" => "foo/bar" +--- "foo/../../../bar" => "../../bar" +--- "/home/jdoe/../../../bar" => "/bar" +--- "C:foo/../../baz" => "C:../baz" +--- "C:/foo/../../baz" => "C:/baz" +--- [[\\?\UNC\server\share\foo\..\..\..\bar]] => "//?/UNC/server/share/bar" +--- ``` +--- +---@since 10 +---@param path (string) Path to normalize +---@param opts? vim.fs.normalize.Opts +---@return (string) : Normalized path +function M.normalize(path, opts) + opts = opts or {} + + if not opts._fast then + vim.validate('path', path, 'string') + vim.validate('expand_env', opts.expand_env, 'boolean', true) + vim.validate('win', opts.win, 'boolean', true) + end + + local win = opts.win == nil and iswin or not not opts.win + local os_sep_local = win and '\\' or '/' + + -- Empty path is already normalized + if path == '' then + return '' + end + + -- Expand ~ to user's home directory + path = expand_home(path, os_sep_local) + + -- Expand environment variables if `opts.expand_env` isn't `false` + if opts.expand_env == nil or opts.expand_env then + path = path:gsub('%$([%w_]+)', uv.os_getenv) --- @type string + end + + if win then + -- Convert path separator to `/` + path = path:gsub(os_sep_local, '/') + end + + -- Check for double slashes at the start of the path because they have special meaning + local double_slash = false + if not opts._fast then + double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///') + end + + local prefix = '' + + if win then + local is_valid --- @type boolean + -- Split Windows paths into prefix and body to make processing easier + prefix, path, is_valid = split_windows_path(path) + + -- If path is not valid, return it as-is + if not is_valid then + return prefix .. path + end + + -- Ensure capital drive and remove extraneous slashes from the prefix + prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/') + end + + if not opts._fast then + -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix + -- and path. + path = path_resolve_dot(path) + end + + -- Preserve leading double slashes as they indicate UNC paths and DOS device paths in + -- Windows and have implementation-defined behavior in POSIX. + path = (double_slash and '/' or '') .. prefix .. path + + -- Change empty path to `.` + if path == '' then + path = '.' + end + + return path +end + +--- @class vim.fs.mkdir.Opts +--- @inlinedoc +--- +--- Create intermediate directories as necessary. +--- (default: `false`) +--- @field parents? boolean +--- +--- Permission bits for newly-created directories. +--- (default: `493`) +--- @field mode? integer + +--- Creates a directory. +--- +---@since 15 +---@param path string Path to create (not expanded/resolved). +---@param opts? vim.fs.mkdir.Opts Optional keyword arguments. +function M.mkdir(path, opts) + vim.validate('path', path, 'string') + vim.validate('opts', opts, 'table', true) + opts = opts or {} + vim.validate('parents', opts.parents, 'boolean', true) + vim.validate('mode', opts.mode, 'number', true) + + vim.fn.mkdir(path, opts.parents and 'p' or '', tostring(opts.mode or 493)) +end + +--- @param path string Path to remove +--- @param ty string type of path +--- @param recursive? boolean +--- @param force? boolean +local function rm(path, ty, recursive, force) + --- @diagnostic disable-next-line:no-unknown + local rm_fn + + if ty == 'directory' then + if recursive then + for file, fty in vim.fs.dir(path) do + rm(M.joinpath(path, file), fty, true, force) + end + elseif not force then + error(string.format('%s is a directory', path)) + end + + rm_fn = uv.fs_rmdir + else + rm_fn = uv.fs_unlink + end + + local ret, err, errnm = rm_fn(path) + if ret == nil and (not force or errnm ~= 'ENOENT') then + error(err) + end +end + +--- @class vim.fs.rm.Opts +--- @inlinedoc +--- +--- Remove directory contents recursively. +--- @field recursive? boolean +--- +--- Ignore nonexistent files and arguments. +--- @field force? boolean + +--- Removes a file or directory. +--- +--- Removes symlinks without touching the origin. To remove the origin, resolve it explicitly +--- with |uv.fs_realpath()|: +--- ```lua +--- vim.fs.rm(vim.uv.fs_realpath('symlink-dir'), { recursive = true }) +--- ``` +--- +--- @since 13 +--- @param path string Path to remove (not expanded/resolved). +--- @param opts? vim.fs.rm.Opts +function M.rm(path, opts) + opts = opts or {} + + local stat, err, errnm = uv.fs_lstat(path) + if stat then + rm(path, stat.type, opts.recursive, opts.force) + elseif not opts.force or errnm ~= 'ENOENT' then + error(err) + end +end + +--- Converts `path` to an absolute path. Expands tilde (~) at the beginning of the path +--- to the user's home directory. Does not check if the path exists, normalize the path, resolve +--- symlinks or hardlinks (including `.` and `..`), or expand environment variables. If the path is +--- already absolute, it is returned unchanged. Also converts `\` path separators to `/`. +--- +--- @since 13 +--- @param path string Path +--- @return string Absolute path +function M.abspath(path) + -- TODO(justinmk): mark f_fnamemodify as API_FAST and use it, ":p:h" should be safe... + + vim.validate('path', path, 'string') + + -- Expand ~ to user's home directory + path = expand_home(path) + + -- Convert path separator to `/` + path = path:gsub(os_sep, '/') + + local prefix = '' + + if iswin then + prefix, path = split_windows_path(path) + end + + if prefix == '//' or vim.startswith(path, '/') then + -- Path is already absolute, do nothing + return prefix .. path + end + + -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory + -- of the drive specified in the path + local cwd = assert((iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or uv.cwd()) + -- Convert cwd path separator to `/` + cwd = cwd:gsub(os_sep, '/') + + if path == '.' then + return cwd + end + -- Prefix is not needed for expanding relative paths, `cwd` already contains it. + return M.joinpath(cwd, path) +end + +--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor. +--- +--- Example: +--- +--- ```lua +--- vim.fs.relpath('/var', '/var/lib') -- 'lib' +--- vim.fs.relpath('/var', '/usr/bin') -- nil +--- ``` +--- +--- @since 13 +--- @param base string +--- @param target string +--- @param opts table? Reserved for future use +--- @return string|nil +function M.relpath(base, target, opts) + vim.validate('base', base, 'string') + vim.validate('target', target, 'string') + vim.validate('opts', opts, 'table', true) + + base = M.normalize(M.abspath(base)) + target = M.normalize(M.abspath(target)) + if base == target then + return '.' + end + + local prefix = '' + if iswin then + prefix, base = split_windows_path(base) + end + base = prefix .. base .. (base ~= '/' and '/' or '') + + return vim.startswith(target, base) and target:sub(#base + 1) or nil +end + +--- Return the file's last extension, if any. +--- +--- Similar to |fnamemodify()| with the |::e| modifier. The extension does not include a leading +--- period. +--- +--- Examples: +--- +--- ```lua +--- vim.fs.ext('archive.tar.gz') -- 'gz' +--- vim.fs.ext('~/.git') -- '' +--- vim.fs.ext('plugin/myplug.lua') -- 'lua' +--- ``` +--- +---@since 14 +---@param file string Path +---@param opts table? Reserved for future use +---@return string Extension of {file} +function M.ext(file, opts) + vim.validate('file', file, 'string') + vim.validate('opts', opts, 'table', true) + return vim.fn.fnamemodify(file, ':e') +end + +return M diff --git a/tests/testdata/lua_neovim_gen_help_html_change/expected.json b/tests/testdata/lua_neovim_gen_help_html_change/expected.json new file mode 100644 index 0000000..8bf6bbc --- /dev/null +++ b/tests/testdata/lua_neovim_gen_help_html_change/expected.json @@ -0,0 +1,6418 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 3, + 1 + ], + "type": "comment", + "start_byte": 24591, + "end_byte": 24666 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 3 + ], + "type": "block", + "start_byte": 24570, + "end_byte": 24734 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 3, + 2 + ], + "type": "comment", + "start_byte": 24673, + "end_byte": 24703 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 3 + ], + "type": "block", + "start_byte": 24570, + "end_byte": 24734 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 3, + 3 + ], + "type": "assignment_statement", + "start_byte": 24710, + "end_byte": 24734 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 3 + ], + "type": "block", + "start_byte": 24570, + "end_byte": 24734 + }, + "position": 3, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 0 + ], + "type": "assignment_statement", + "start_byte": 24750, + "end_byte": 24791 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1 + ], + "type": "block", + "start_byte": 24750, + "end_byte": 25553 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 24922, + "end_byte": 24949 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 3 + ], + "type": "block", + "start_byte": 24899, + "end_byte": 24949 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 3 + ], + "type": "comment", + "start_byte": 25186, + "end_byte": 25258 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5 + ], + "type": "elseif_statement", + "start_byte": 25149, + "end_byte": 25543 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5 + ], + "type": "block", + "start_byte": 25336, + "end_byte": 25543 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5 + ], + "type": "elseif_statement", + "start_byte": 25149, + "end_byte": 25543 + }, + "position": 5 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 4, + 4, + 1 + ], + "type": "assignment_statement", + "start_byte": 25107, + "end_byte": 25142 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 4, + 4 + ], + "type": "block", + "start_byte": 25071, + "end_byte": 25142 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 1 + ], + "type": "assignment_statement", + "start_byte": 25508, + "end_byte": 25543 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5 + ], + "type": "block", + "start_byte": 25336, + "end_byte": 25543 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 0 + ], + "type": "while", + "label": "while", + "start_byte": 25336, + "end_byte": 25341 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0 + ], + "type": "while_statement", + "start_byte": 25336, + "end_byte": 25499 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 1 + ], + "type": "binary_expression", + "start_byte": 25342, + "end_byte": 25404 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0 + ], + "type": "while_statement", + "start_byte": 25336, + "end_byte": 25499 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 2 + ], + "type": "do", + "label": "do", + "start_byte": 25405, + "end_byte": 25407 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0 + ], + "type": "while_statement", + "start_byte": 25336, + "end_byte": 25499 + }, + "position": 2 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0, + 0 + ], + "type": "variable_list", + "start_byte": 25004, + "end_byte": 25014 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 25460, + "end_byte": 25487 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0 + ], + "type": "assignment_statement", + "start_byte": 25004, + "end_byte": 25044 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 25460, + "dest_end_byte": 25470 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 25015, + "end_byte": 25016 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 3, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 25471, + "end_byte": 25472 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 25460, + "end_byte": 25487 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 4 + ], + "type": "end", + "label": "end", + "start_byte": 25496, + "end_byte": 25499 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0 + ], + "type": "while_statement", + "start_byte": 25336, + "end_byte": 25499 + }, + "position": 4 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 3, + 0 + ], + "type": "assignment_statement", + "start_byte": 25418, + "end_byte": 25449 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 3 + ], + "type": "block", + "start_byte": 25418, + "end_byte": 25487 + }, + "position": 0, + "subtree": true + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0, + 2, + 0, + 1, + 1 + ], + "type": "binary_expression", + "start_byte": 25029, + "end_byte": 25043 + }, + "parent": { + "tree": "after", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 3, + 5, + 5, + 0, + 3, + 1, + 2 + ], + "type": "expression_list", + "start_byte": 25473, + "end_byte": 25487 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0, + 2, + 0, + 1 + ], + "type": "arguments", + "start_byte": 25025, + "end_byte": 25044 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 25473, + "dest_end_byte": 25487 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0, + 2, + 0, + 0 + ], + "type": "dot_index_expression", + "start_byte": 25017, + "end_byte": 25025 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 155, + 4, + 12, + 14, + 3, + 8, + 4, + 1, + 2, + 5, + 4, + 0, + 2, + 0, + 1, + 0 + ], + "type": "number", + "label": "1", + "start_byte": 25026, + "end_byte": 25027 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": 376, + "right_line": 376 + }, + { + "left_line": 377, + "right_line": 377 + }, + { + "left_line": 378, + "right_line": 378 + }, + { + "left_line": 379, + "right_line": 379 + }, + { + "left_line": 380, + "right_line": 380 + }, + { + "left_line": 381, + "right_line": 381 + }, + { + "left_line": 382, + "right_line": 382 + }, + { + "left_line": 383, + "right_line": 383 + }, + { + "left_line": 384, + "right_line": 384 + }, + { + "left_line": 385, + "right_line": 385 + }, + { + "left_line": 386, + "right_line": 386 + }, + { + "left_line": 387, + "right_line": 387 + }, + { + "left_line": 388, + "right_line": 388 + }, + { + "left_line": 389, + "right_line": 389 + }, + { + "left_line": 390, + "right_line": 390 + }, + { + "left_line": 391, + "right_line": 391 + }, + { + "left_line": 392, + "right_line": 392 + }, + { + "left_line": 393, + "right_line": 393 + }, + { + "left_line": 394, + "right_line": 394 + }, + { + "left_line": 395, + "right_line": 395 + }, + { + "left_line": 396, + "right_line": 396 + }, + { + "left_line": 397, + "right_line": 397 + }, + { + "left_line": 398, + "right_line": 398 + }, + { + "left_line": 399, + "right_line": 399 + }, + { + "left_line": 400, + "right_line": 400 + }, + { + "left_line": 401, + "right_line": 401 + }, + { + "left_line": 402, + "right_line": 402 + }, + { + "left_line": 403, + "right_line": 403 + }, + { + "left_line": 404, + "right_line": 404 + }, + { + "left_line": 405, + "right_line": 405 + }, + { + "left_line": 406, + "right_line": 406 + }, + { + "left_line": 407, + "right_line": 407 + }, + { + "left_line": 408, + "right_line": 408 + }, + { + "left_line": 409, + "right_line": 409 + }, + { + "left_line": 410, + "right_line": 410 + }, + { + "left_line": 411, + "right_line": 411 + }, + { + "left_line": 412, + "right_line": 412 + }, + { + "left_line": 413, + "right_line": 413 + }, + { + "left_line": 414, + "right_line": 414 + }, + { + "left_line": 415, + "right_line": 415 + }, + { + "left_line": 416, + "right_line": 416 + }, + { + "left_line": 417, + "right_line": 417 + }, + { + "left_line": 418, + "right_line": 418 + }, + { + "left_line": 419, + "right_line": 419 + }, + { + "left_line": 420, + "right_line": 420 + }, + { + "left_line": 421, + "right_line": 421 + }, + { + "left_line": 422, + "right_line": 422 + }, + { + "left_line": 423, + "right_line": 423 + }, + { + "left_line": 424, + "right_line": 424 + }, + { + "left_line": 425, + "right_line": 425 + }, + { + "left_line": 426, + "right_line": 426 + }, + { + "left_line": 427, + "right_line": 427 + }, + { + "left_line": 428, + "right_line": 428 + }, + { + "left_line": 429, + "right_line": 429 + }, + { + "left_line": 430, + "right_line": 430 + }, + { + "left_line": 431, + "right_line": 431 + }, + { + "left_line": 432, + "right_line": 432 + }, + { + "left_line": 433, + "right_line": 433 + }, + { + "left_line": 434, + "right_line": 434 + }, + { + "left_line": 435, + "right_line": 435 + }, + { + "left_line": 436, + "right_line": 436 + }, + { + "left_line": 437, + "right_line": 437 + }, + { + "left_line": 438, + "right_line": 438 + }, + { + "left_line": 439, + "right_line": 439 + }, + { + "left_line": 440, + "right_line": 440 + }, + { + "left_line": 441, + "right_line": 441 + }, + { + "left_line": 442, + "right_line": 442 + }, + { + "left_line": 443, + "right_line": 443 + }, + { + "left_line": 444, + "right_line": 444 + }, + { + "left_line": 445, + "right_line": 445 + }, + { + "left_line": 446, + "right_line": 446 + }, + { + "left_line": 447, + "right_line": 447 + }, + { + "left_line": 448, + "right_line": 448 + }, + { + "left_line": 449, + "right_line": 449 + }, + { + "left_line": 450, + "right_line": 450 + }, + { + "left_line": 451, + "right_line": 451 + }, + { + "left_line": 452, + "right_line": 452 + }, + { + "left_line": 453, + "right_line": 453 + }, + { + "left_line": 454, + "right_line": 454 + }, + { + "left_line": 455, + "right_line": 455 + }, + { + "left_line": 456, + "right_line": 456 + }, + { + "left_line": 457, + "right_line": 457 + }, + { + "left_line": 458, + "right_line": 458 + }, + { + "left_line": 459, + "right_line": 459 + }, + { + "left_line": 460, + "right_line": 460 + }, + { + "left_line": 461, + "right_line": 461 + }, + { + "left_line": 462, + "right_line": 462 + }, + { + "left_line": 463, + "right_line": 463 + }, + { + "left_line": 464, + "right_line": 464 + }, + { + "left_line": 465, + "right_line": 465 + }, + { + "left_line": 466, + "right_line": 466 + }, + { + "left_line": 467, + "right_line": 467 + }, + { + "left_line": 468, + "right_line": 468 + }, + { + "left_line": 469, + "right_line": 469 + }, + { + "left_line": 470, + "right_line": 470 + }, + { + "left_line": 471, + "right_line": 471 + }, + { + "left_line": 472, + "right_line": 472 + }, + { + "left_line": 473, + "right_line": 473 + }, + { + "left_line": 474, + "right_line": 474 + }, + { + "left_line": 475, + "right_line": 475 + }, + { + "left_line": 476, + "right_line": 476 + }, + { + "left_line": 477, + "right_line": 477 + }, + { + "left_line": 478, + "right_line": 478 + }, + { + "left_line": 479, + "right_line": 479 + }, + { + "left_line": 480, + "right_line": 480 + }, + { + "left_line": 481, + "right_line": 481 + }, + { + "left_line": 482, + "right_line": 482 + }, + { + "left_line": 483, + "right_line": 483 + }, + { + "left_line": 484, + "right_line": 484 + }, + { + "left_line": 485, + "right_line": 485 + }, + { + "left_line": 486, + "right_line": 486 + }, + { + "left_line": 487, + "right_line": 487 + }, + { + "left_line": 488, + "right_line": 488 + }, + { + "left_line": 489, + "right_line": 489 + }, + { + "left_line": 490, + "right_line": 490 + }, + { + "left_line": 491, + "right_line": 491 + }, + { + "left_line": 492, + "right_line": 492 + }, + { + "left_line": 493, + "right_line": 493 + }, + { + "left_line": 494, + "right_line": 494 + }, + { + "left_line": 495, + "right_line": 495 + }, + { + "left_line": 496, + "right_line": 496 + }, + { + "left_line": 497, + "right_line": 497 + }, + { + "left_line": 498, + "right_line": 498 + }, + { + "left_line": 499, + "right_line": 499 + }, + { + "left_line": 500, + "right_line": 500 + }, + { + "left_line": 501, + "right_line": 501 + }, + { + "left_line": 502, + "right_line": 502 + }, + { + "left_line": 503, + "right_line": 503 + }, + { + "left_line": 504, + "right_line": 504 + }, + { + "left_line": 505, + "right_line": 505 + }, + { + "left_line": 506, + "right_line": 506 + }, + { + "left_line": 507, + "right_line": 507 + }, + { + "left_line": 508, + "right_line": 508 + }, + { + "left_line": 509, + "right_line": 509 + }, + { + "left_line": 510, + "right_line": 510 + }, + { + "left_line": 511, + "right_line": 511 + }, + { + "left_line": 512, + "right_line": 512 + }, + { + "left_line": 513, + "right_line": 513 + }, + { + "left_line": 514, + "right_line": 514 + }, + { + "left_line": 515, + "right_line": 515 + }, + { + "left_line": 516, + "right_line": 516 + }, + { + "left_line": 517, + "right_line": 517 + }, + { + "left_line": 518, + "right_line": 518 + }, + { + "left_line": 519, + "right_line": 519 + }, + { + "left_line": 520, + "right_line": 520 + }, + { + "left_line": 521, + "right_line": 521 + }, + { + "left_line": 522, + "right_line": 522 + }, + { + "left_line": 523, + "right_line": 523 + }, + { + "left_line": 524, + "right_line": 524 + }, + { + "left_line": 525, + "right_line": 525 + }, + { + "left_line": 526, + "right_line": 526 + }, + { + "left_line": 527, + "right_line": 527 + }, + { + "left_line": 528, + "right_line": 528 + }, + { + "left_line": 529, + "right_line": 529 + }, + { + "left_line": 530, + "right_line": 530 + }, + { + "left_line": 531, + "right_line": 531 + }, + { + "left_line": 532, + "right_line": 532 + }, + { + "left_line": 533, + "right_line": 533 + }, + { + "left_line": 534, + "right_line": 534 + }, + { + "left_line": 535, + "right_line": 535 + }, + { + "left_line": 536, + "right_line": 536 + }, + { + "left_line": 537, + "right_line": 537 + }, + { + "left_line": 538, + "right_line": 538 + }, + { + "left_line": 539, + "right_line": 539 + }, + { + "left_line": 540, + "right_line": 540 + }, + { + "left_line": 541, + "right_line": 541 + }, + { + "left_line": 542, + "right_line": 542 + }, + { + "left_line": 543, + "right_line": 543 + }, + { + "left_line": 544, + "right_line": 544 + }, + { + "left_line": 545, + "right_line": 545 + }, + { + "left_line": 546, + "right_line": 546 + }, + { + "left_line": 547, + "right_line": 547 + }, + { + "left_line": 548, + "right_line": 548 + }, + { + "left_line": 549, + "right_line": 549 + }, + { + "left_line": 550, + "right_line": 550 + }, + { + "left_line": 551, + "right_line": 551 + }, + { + "left_line": 552, + "right_line": 552 + }, + { + "left_line": 553, + "right_line": 553 + }, + { + "left_line": 554, + "right_line": 554 + }, + { + "left_line": 555, + "right_line": 555 + }, + { + "left_line": 556, + "right_line": 556 + }, + { + "left_line": 557, + "right_line": 557 + }, + { + "left_line": 558, + "right_line": 558 + }, + { + "left_line": 559, + "right_line": 559 + }, + { + "left_line": 560, + "right_line": 560 + }, + { + "left_line": 561, + "right_line": 561 + }, + { + "left_line": 562, + "right_line": 562 + }, + { + "left_line": 563, + "right_line": 563 + }, + { + "left_line": 564, + "right_line": 564 + }, + { + "left_line": 565, + "right_line": 565 + }, + { + "left_line": 566, + "right_line": 566 + }, + { + "left_line": 567, + "right_line": 567 + }, + { + "left_line": 568, + "right_line": 568 + }, + { + "left_line": 569, + "right_line": 569 + }, + { + "left_line": 570, + "right_line": 570 + }, + { + "left_line": 571, + "right_line": 571 + }, + { + "left_line": 572, + "right_line": 572 + }, + { + "left_line": 573, + "right_line": 573 + }, + { + "left_line": 574, + "right_line": 574 + }, + { + "left_line": 575, + "right_line": 575 + }, + { + "left_line": 576, + "right_line": 576 + }, + { + "left_line": 577, + "right_line": 577 + }, + { + "left_line": 578, + "right_line": 578 + }, + { + "left_line": 579, + "right_line": 579 + }, + { + "left_line": 580, + "right_line": 580 + }, + { + "left_line": 581, + "right_line": 581 + }, + { + "left_line": 582, + "right_line": 582 + }, + { + "left_line": 583, + "right_line": 583 + }, + { + "left_line": 584, + "right_line": 584 + }, + { + "left_line": 585, + "right_line": 585 + }, + { + "left_line": 586, + "right_line": 586 + }, + { + "left_line": 587, + "right_line": 587 + }, + { + "left_line": 588, + "right_line": 588 + }, + { + "left_line": 589, + "right_line": 589 + }, + { + "left_line": 590, + "right_line": 590 + }, + { + "left_line": 591, + "right_line": 591 + }, + { + "left_line": 592, + "right_line": 592 + }, + { + "left_line": 593, + "right_line": 593 + }, + { + "left_line": 594, + "right_line": 594 + }, + { + "left_line": 595, + "right_line": 595 + }, + { + "left_line": 596, + "right_line": 596 + }, + { + "left_line": 597, + "right_line": 597 + }, + { + "left_line": 598, + "right_line": 598 + }, + { + "left_line": 599, + "right_line": 599 + }, + { + "left_line": 600, + "right_line": 600 + }, + { + "left_line": 601, + "right_line": 601 + }, + { + "left_line": 602, + "right_line": 602 + }, + { + "left_line": 603, + "right_line": 603 + }, + { + "left_line": 604, + "right_line": 604 + }, + { + "left_line": 605, + "right_line": 605 + }, + { + "left_line": 606, + "right_line": 606 + }, + { + "left_line": 607, + "right_line": 607 + }, + { + "left_line": 608, + "right_line": 608 + }, + { + "left_line": 609, + "right_line": 609 + }, + { + "left_line": 610, + "right_line": 610 + }, + { + "left_line": 611, + "right_line": 611 + }, + { + "left_line": 612, + "right_line": 612 + }, + { + "left_line": 613, + "right_line": 613 + }, + { + "left_line": 614, + "right_line": 614 + }, + { + "left_line": 615, + "right_line": 615 + }, + { + "left_line": 616, + "right_line": 616 + }, + { + "left_line": 617, + "right_line": 617 + }, + { + "left_line": 618, + "right_line": 618 + }, + { + "left_line": 619, + "right_line": 619 + }, + { + "left_line": 620, + "right_line": 620 + }, + { + "left_line": 621, + "right_line": 621 + }, + { + "left_line": 622, + "right_line": 622 + }, + { + "left_line": 623, + "right_line": 623 + }, + { + "left_line": 624, + "right_line": 624 + }, + { + "left_line": 625, + "right_line": 625 + }, + { + "left_line": 626, + "right_line": 626 + }, + { + "left_line": 627, + "right_line": 627 + }, + { + "left_line": 628, + "right_line": 628 + }, + { + "left_line": 629, + "right_line": 629 + }, + { + "left_line": 630, + "right_line": 630 + }, + { + "left_line": 631, + "right_line": 631 + }, + { + "left_line": 632, + "right_line": 632 + }, + { + "left_line": 633, + "right_line": 633 + }, + { + "left_line": 634, + "right_line": 634 + }, + { + "left_line": 635, + "right_line": 635 + }, + { + "left_line": 636, + "right_line": 636 + }, + { + "left_line": 637, + "right_line": 637 + }, + { + "left_line": 638, + "right_line": 638 + }, + { + "left_line": 639, + "right_line": 639 + }, + { + "left_line": 640, + "right_line": 640 + }, + { + "left_line": 641, + "right_line": 641 + }, + { + "left_line": 642, + "right_line": 642 + }, + { + "left_line": 643, + "right_line": 643 + }, + { + "left_line": 644, + "right_line": 644 + }, + { + "left_line": 645, + "right_line": 645 + }, + { + "left_line": 646, + "right_line": 646 + }, + { + "left_line": 647, + "right_line": 647 + }, + { + "left_line": 648, + "right_line": 648 + }, + { + "left_line": 649, + "right_line": 649 + }, + { + "left_line": 650, + "right_line": 650 + }, + { + "left_line": 651, + "right_line": 651 + }, + { + "left_line": 652, + "right_line": 652 + }, + { + "left_line": 653, + "right_line": 653 + }, + { + "left_line": 654, + "right_line": 654 + }, + { + "left_line": 655, + "right_line": 655 + }, + { + "left_line": 656, + "right_line": 656 + }, + { + "left_line": 657, + "right_line": 657 + }, + { + "left_line": 658, + "right_line": 658 + }, + { + "left_line": 659, + "right_line": 659 + }, + { + "left_line": 660, + "right_line": 660 + }, + { + "left_line": 661, + "right_line": 661 + }, + { + "left_line": 662, + "right_line": 662 + }, + { + "left_line": 663, + "right_line": 663 + }, + { + "left_line": 664, + "right_line": 664 + }, + { + "left_line": 665, + "right_line": 665 + }, + { + "left_line": 666, + "right_line": 666 + }, + { + "left_line": 667, + "right_line": 667 + }, + { + "left_line": 668, + "right_line": 668 + }, + { + "left_line": 669, + "right_line": 669 + }, + { + "left_line": 670, + "right_line": 670 + }, + { + "left_line": 671, + "right_line": 671 + }, + { + "left_line": 672, + "right_line": 672 + }, + { + "left_line": 673, + "right_line": 673 + }, + { + "left_line": 674, + "right_line": 674 + }, + { + "left_line": 675, + "right_line": 675 + }, + { + "left_line": 676, + "right_line": 676 + }, + { + "left_line": 677, + "right_line": 677 + }, + { + "left_line": 678, + "right_line": 678 + }, + { + "left_line": 679, + "right_line": 679 + }, + { + "left_line": 680, + "right_line": 680 + }, + { + "left_line": 681, + "right_line": 681 + }, + { + "left_line": 682, + "right_line": 682 + }, + { + "left_line": 683, + "right_line": 683 + }, + { + "left_line": 684, + "right_line": 684 + }, + { + "left_line": 685, + "right_line": 685 + }, + { + "left_line": 686, + "right_line": 686 + }, + { + "left_line": 687, + "right_line": 687 + }, + { + "left_line": 688, + "right_line": 688 + }, + { + "left_line": 689, + "right_line": 689 + }, + { + "left_line": 690, + "right_line": 690 + }, + { + "left_line": 691, + "right_line": 691 + }, + { + "left_line": 692, + "right_line": 692 + }, + { + "left_line": 693, + "right_line": 693 + }, + { + "left_line": 694, + "right_line": 694 + }, + { + "left_line": 695, + "right_line": 695 + }, + { + "left_line": 696, + "right_line": 696 + }, + { + "left_line": 697, + "right_line": 697 + }, + { + "left_line": 698, + "right_line": 698 + }, + { + "left_line": 699, + "right_line": 699 + }, + { + "left_line": 700, + "right_line": 700 + }, + { + "left_line": 701, + "right_line": 701 + }, + { + "left_line": 702, + "right_line": 702 + }, + { + "left_line": 703, + "right_line": 703 + }, + { + "left_line": 704, + "right_line": 704 + }, + { + "left_line": 705, + "right_line": 705 + }, + { + "left_line": -1, + "right_line": 706 + }, + { + "left_line": -1, + "right_line": 707 + }, + { + "left_line": -1, + "right_line": 708 + }, + { + "left_line": 706, + "right_line": 709 + }, + { + "left_line": -1, + "right_line": 710 + }, + { + "left_line": 707, + "right_line": 711 + }, + { + "left_line": 708, + "right_line": 712 + }, + { + "left_line": 709, + "right_line": 713 + }, + { + "left_line": 710, + "right_line": 714 + }, + { + "left_line": -1, + "right_line": 715 + }, + { + "left_line": 711, + "right_line": 716 + }, + { + "left_line": 712, + "right_line": 717 + }, + { + "left_line": 713, + "right_line": 718 + }, + { + "left_line": -1, + "right_line": 719 + }, + { + "left_line": 714, + "right_line": 720 + }, + { + "left_line": -1, + "right_line": 721 + }, + { + "left_line": 715, + "right_line": 722 + }, + { + "left_line": -1, + "right_line": 723 + }, + { + "left_line": -1, + "right_line": 724 + }, + { + "left_line": 716, + "right_line": 725 + }, + { + "left_line": -1, + "right_line": 726 + }, + { + "left_line": -1, + "right_line": 727 + }, + { + "left_line": 717, + "right_line": 728 + }, + { + "left_line": 718, + "right_line": 729 + }, + { + "left_line": 719, + "right_line": 730 + }, + { + "left_line": 720, + "right_line": 731 + }, + { + "left_line": 721, + "right_line": 732 + }, + { + "left_line": 722, + "right_line": 733 + }, + { + "left_line": 723, + "right_line": 734 + }, + { + "left_line": 724, + "right_line": 735 + }, + { + "left_line": 725, + "right_line": 736 + }, + { + "left_line": 726, + "right_line": 737 + }, + { + "left_line": 727, + "right_line": 738 + }, + { + "left_line": 728, + "right_line": 739 + }, + { + "left_line": 729, + "right_line": 740 + }, + { + "left_line": 730, + "right_line": 741 + }, + { + "left_line": 731, + "right_line": 742 + }, + { + "left_line": 732, + "right_line": 743 + }, + { + "left_line": 733, + "right_line": 744 + }, + { + "left_line": 734, + "right_line": 745 + }, + { + "left_line": 735, + "right_line": 746 + }, + { + "left_line": 736, + "right_line": 747 + }, + { + "left_line": 737, + "right_line": 748 + }, + { + "left_line": 738, + "right_line": 749 + }, + { + "left_line": 739, + "right_line": 750 + }, + { + "left_line": 740, + "right_line": 751 + }, + { + "left_line": 741, + "right_line": 752 + }, + { + "left_line": 742, + "right_line": 753 + }, + { + "left_line": 743, + "right_line": 754 + }, + { + "left_line": 744, + "right_line": 755 + }, + { + "left_line": 745, + "right_line": 756 + }, + { + "left_line": 746, + "right_line": 757 + }, + { + "left_line": 747, + "right_line": 758 + }, + { + "left_line": 748, + "right_line": 759 + }, + { + "left_line": 749, + "right_line": 760 + }, + { + "left_line": 750, + "right_line": 761 + }, + { + "left_line": 751, + "right_line": 762 + }, + { + "left_line": 752, + "right_line": 763 + }, + { + "left_line": 753, + "right_line": 764 + }, + { + "left_line": 754, + "right_line": 765 + }, + { + "left_line": 755, + "right_line": 766 + }, + { + "left_line": 756, + "right_line": 767 + }, + { + "left_line": 757, + "right_line": 768 + }, + { + "left_line": 758, + "right_line": 769 + }, + { + "left_line": 759, + "right_line": 770 + }, + { + "left_line": 760, + "right_line": 771 + }, + { + "left_line": 761, + "right_line": 772 + }, + { + "left_line": 762, + "right_line": 773 + }, + { + "left_line": 763, + "right_line": 774 + }, + { + "left_line": 764, + "right_line": 775 + }, + { + "left_line": 765, + "right_line": 776 + }, + { + "left_line": 766, + "right_line": 777 + }, + { + "left_line": 767, + "right_line": 778 + }, + { + "left_line": 768, + "right_line": 779 + }, + { + "left_line": 769, + "right_line": 780 + }, + { + "left_line": 770, + "right_line": 781 + }, + { + "left_line": 771, + "right_line": 782 + }, + { + "left_line": 772, + "right_line": 783 + }, + { + "left_line": 773, + "right_line": 784 + }, + { + "left_line": 774, + "right_line": 785 + }, + { + "left_line": 775, + "right_line": 786 + }, + { + "left_line": 776, + "right_line": 787 + }, + { + "left_line": 777, + "right_line": 788 + }, + { + "left_line": 778, + "right_line": 789 + }, + { + "left_line": 779, + "right_line": 790 + }, + { + "left_line": 780, + "right_line": 791 + }, + { + "left_line": 781, + "right_line": 792 + }, + { + "left_line": 782, + "right_line": 793 + }, + { + "left_line": 783, + "right_line": 794 + }, + { + "left_line": 784, + "right_line": 795 + }, + { + "left_line": 785, + "right_line": 796 + }, + { + "left_line": 786, + "right_line": 797 + }, + { + "left_line": 787, + "right_line": 798 + }, + { + "left_line": 788, + "right_line": 799 + }, + { + "left_line": 789, + "right_line": 800 + }, + { + "left_line": 790, + "right_line": 801 + }, + { + "left_line": 791, + "right_line": 802 + }, + { + "left_line": 792, + "right_line": 803 + }, + { + "left_line": 793, + "right_line": 804 + }, + { + "left_line": 794, + "right_line": 805 + }, + { + "left_line": 795, + "right_line": 806 + }, + { + "left_line": 796, + "right_line": 807 + }, + { + "left_line": 797, + "right_line": 808 + }, + { + "left_line": 798, + "right_line": 809 + }, + { + "left_line": 799, + "right_line": 810 + }, + { + "left_line": 800, + "right_line": 811 + }, + { + "left_line": 801, + "right_line": 812 + }, + { + "left_line": 802, + "right_line": 813 + }, + { + "left_line": 803, + "right_line": 814 + }, + { + "left_line": 804, + "right_line": 815 + }, + { + "left_line": 805, + "right_line": 816 + }, + { + "left_line": 806, + "right_line": 817 + }, + { + "left_line": 807, + "right_line": 818 + }, + { + "left_line": 808, + "right_line": 819 + }, + { + "left_line": 809, + "right_line": 820 + }, + { + "left_line": 810, + "right_line": 821 + }, + { + "left_line": 811, + "right_line": 822 + }, + { + "left_line": 812, + "right_line": 823 + }, + { + "left_line": 813, + "right_line": 824 + }, + { + "left_line": 814, + "right_line": 825 + }, + { + "left_line": 815, + "right_line": 826 + }, + { + "left_line": 816, + "right_line": 827 + }, + { + "left_line": 817, + "right_line": 828 + }, + { + "left_line": 818, + "right_line": 829 + }, + { + "left_line": 819, + "right_line": 830 + }, + { + "left_line": 820, + "right_line": 831 + }, + { + "left_line": 821, + "right_line": 832 + }, + { + "left_line": 822, + "right_line": 833 + }, + { + "left_line": 823, + "right_line": 834 + }, + { + "left_line": 824, + "right_line": 835 + }, + { + "left_line": 825, + "right_line": 836 + }, + { + "left_line": 826, + "right_line": 837 + }, + { + "left_line": 827, + "right_line": 838 + }, + { + "left_line": 828, + "right_line": 839 + }, + { + "left_line": 829, + "right_line": 840 + }, + { + "left_line": 830, + "right_line": 841 + }, + { + "left_line": 831, + "right_line": 842 + }, + { + "left_line": 832, + "right_line": 843 + }, + { + "left_line": 833, + "right_line": 844 + }, + { + "left_line": 834, + "right_line": 845 + }, + { + "left_line": 835, + "right_line": 846 + }, + { + "left_line": 836, + "right_line": 847 + }, + { + "left_line": 837, + "right_line": 848 + }, + { + "left_line": 838, + "right_line": 849 + }, + { + "left_line": 839, + "right_line": 850 + }, + { + "left_line": 840, + "right_line": 851 + }, + { + "left_line": 841, + "right_line": 852 + }, + { + "left_line": 842, + "right_line": 853 + }, + { + "left_line": 843, + "right_line": 854 + }, + { + "left_line": 844, + "right_line": 855 + }, + { + "left_line": 845, + "right_line": 856 + }, + { + "left_line": 846, + "right_line": 857 + }, + { + "left_line": 847, + "right_line": 858 + }, + { + "left_line": 848, + "right_line": 859 + }, + { + "left_line": 849, + "right_line": 860 + }, + { + "left_line": 850, + "right_line": 861 + }, + { + "left_line": 851, + "right_line": 862 + }, + { + "left_line": 852, + "right_line": 863 + }, + { + "left_line": 853, + "right_line": 864 + }, + { + "left_line": 854, + "right_line": 865 + }, + { + "left_line": 855, + "right_line": 866 + }, + { + "left_line": 856, + "right_line": 867 + }, + { + "left_line": 857, + "right_line": 868 + }, + { + "left_line": 858, + "right_line": 869 + }, + { + "left_line": 859, + "right_line": 870 + }, + { + "left_line": 860, + "right_line": 871 + }, + { + "left_line": 861, + "right_line": 872 + }, + { + "left_line": 862, + "right_line": 873 + }, + { + "left_line": 863, + "right_line": 874 + }, + { + "left_line": 864, + "right_line": 875 + }, + { + "left_line": 865, + "right_line": 876 + }, + { + "left_line": 866, + "right_line": 877 + }, + { + "left_line": 867, + "right_line": 878 + }, + { + "left_line": 868, + "right_line": 879 + }, + { + "left_line": 869, + "right_line": 880 + }, + { + "left_line": 870, + "right_line": 881 + }, + { + "left_line": 871, + "right_line": 882 + }, + { + "left_line": 872, + "right_line": 883 + }, + { + "left_line": 873, + "right_line": 884 + }, + { + "left_line": 874, + "right_line": 885 + }, + { + "left_line": 875, + "right_line": 886 + }, + { + "left_line": 876, + "right_line": 887 + }, + { + "left_line": 877, + "right_line": 888 + }, + { + "left_line": 878, + "right_line": 889 + }, + { + "left_line": 879, + "right_line": 890 + }, + { + "left_line": 880, + "right_line": 891 + }, + { + "left_line": 881, + "right_line": 892 + }, + { + "left_line": 882, + "right_line": 893 + }, + { + "left_line": 883, + "right_line": 894 + }, + { + "left_line": 884, + "right_line": 895 + }, + { + "left_line": 885, + "right_line": 896 + }, + { + "left_line": 886, + "right_line": 897 + }, + { + "left_line": 887, + "right_line": 898 + }, + { + "left_line": 888, + "right_line": 899 + }, + { + "left_line": 889, + "right_line": 900 + }, + { + "left_line": 890, + "right_line": 901 + }, + { + "left_line": 891, + "right_line": 902 + }, + { + "left_line": 892, + "right_line": 903 + }, + { + "left_line": 893, + "right_line": 904 + }, + { + "left_line": 894, + "right_line": 905 + }, + { + "left_line": 895, + "right_line": 906 + }, + { + "left_line": 896, + "right_line": 907 + }, + { + "left_line": 897, + "right_line": 908 + }, + { + "left_line": 898, + "right_line": 909 + }, + { + "left_line": 899, + "right_line": 910 + }, + { + "left_line": 900, + "right_line": 911 + }, + { + "left_line": 901, + "right_line": 912 + }, + { + "left_line": 902, + "right_line": 913 + }, + { + "left_line": 903, + "right_line": 914 + }, + { + "left_line": 904, + "right_line": 915 + }, + { + "left_line": 905, + "right_line": 916 + }, + { + "left_line": 906, + "right_line": 917 + }, + { + "left_line": 907, + "right_line": 918 + }, + { + "left_line": 908, + "right_line": 919 + }, + { + "left_line": 909, + "right_line": 920 + }, + { + "left_line": 910, + "right_line": 921 + }, + { + "left_line": 911, + "right_line": 922 + }, + { + "left_line": 912, + "right_line": 923 + }, + { + "left_line": 913, + "right_line": 924 + }, + { + "left_line": 914, + "right_line": 925 + }, + { + "left_line": 915, + "right_line": 926 + }, + { + "left_line": 916, + "right_line": 927 + }, + { + "left_line": 917, + "right_line": 928 + }, + { + "left_line": 918, + "right_line": 929 + }, + { + "left_line": 919, + "right_line": 930 + }, + { + "left_line": 920, + "right_line": 931 + }, + { + "left_line": 921, + "right_line": 932 + }, + { + "left_line": 922, + "right_line": 933 + }, + { + "left_line": 923, + "right_line": 934 + }, + { + "left_line": 924, + "right_line": 935 + }, + { + "left_line": 925, + "right_line": 936 + }, + { + "left_line": 926, + "right_line": 937 + }, + { + "left_line": 927, + "right_line": 938 + }, + { + "left_line": 928, + "right_line": 939 + }, + { + "left_line": 929, + "right_line": 940 + }, + { + "left_line": 930, + "right_line": 941 + }, + { + "left_line": 931, + "right_line": 942 + }, + { + "left_line": 932, + "right_line": 943 + }, + { + "left_line": 933, + "right_line": 944 + }, + { + "left_line": 934, + "right_line": 945 + }, + { + "left_line": 935, + "right_line": 946 + }, + { + "left_line": 936, + "right_line": 947 + }, + { + "left_line": 937, + "right_line": 948 + }, + { + "left_line": 938, + "right_line": 949 + }, + { + "left_line": 939, + "right_line": 950 + }, + { + "left_line": 940, + "right_line": 951 + }, + { + "left_line": 941, + "right_line": 952 + }, + { + "left_line": 942, + "right_line": 953 + }, + { + "left_line": 943, + "right_line": 954 + }, + { + "left_line": 944, + "right_line": 955 + }, + { + "left_line": 945, + "right_line": 956 + }, + { + "left_line": 946, + "right_line": 957 + }, + { + "left_line": 947, + "right_line": 958 + }, + { + "left_line": 948, + "right_line": 959 + }, + { + "left_line": 949, + "right_line": 960 + }, + { + "left_line": 950, + "right_line": 961 + }, + { + "left_line": 951, + "right_line": 962 + }, + { + "left_line": 952, + "right_line": 963 + }, + { + "left_line": 953, + "right_line": 964 + }, + { + "left_line": 954, + "right_line": 965 + }, + { + "left_line": 955, + "right_line": 966 + }, + { + "left_line": 956, + "right_line": 967 + }, + { + "left_line": 957, + "right_line": 968 + }, + { + "left_line": 958, + "right_line": 969 + }, + { + "left_line": 959, + "right_line": 970 + }, + { + "left_line": 960, + "right_line": 971 + }, + { + "left_line": 961, + "right_line": 972 + }, + { + "left_line": 962, + "right_line": 973 + }, + { + "left_line": 963, + "right_line": 974 + }, + { + "left_line": 964, + "right_line": 975 + }, + { + "left_line": 965, + "right_line": 976 + }, + { + "left_line": 966, + "right_line": 977 + }, + { + "left_line": 967, + "right_line": 978 + }, + { + "left_line": 968, + "right_line": 979 + }, + { + "left_line": 969, + "right_line": 980 + }, + { + "left_line": 970, + "right_line": 981 + }, + { + "left_line": 971, + "right_line": 982 + }, + { + "left_line": 972, + "right_line": 983 + }, + { + "left_line": 973, + "right_line": 984 + }, + { + "left_line": 974, + "right_line": 985 + }, + { + "left_line": 975, + "right_line": 986 + }, + { + "left_line": 976, + "right_line": 987 + }, + { + "left_line": 977, + "right_line": 988 + }, + { + "left_line": 978, + "right_line": 989 + }, + { + "left_line": 979, + "right_line": 990 + }, + { + "left_line": 980, + "right_line": 991 + }, + { + "left_line": 981, + "right_line": 992 + }, + { + "left_line": 982, + "right_line": 993 + }, + { + "left_line": 983, + "right_line": 994 + }, + { + "left_line": 984, + "right_line": 995 + }, + { + "left_line": 985, + "right_line": 996 + }, + { + "left_line": 986, + "right_line": 997 + }, + { + "left_line": 987, + "right_line": 998 + }, + { + "left_line": 988, + "right_line": 999 + }, + { + "left_line": 989, + "right_line": 1000 + }, + { + "left_line": 990, + "right_line": 1001 + }, + { + "left_line": 991, + "right_line": 1002 + }, + { + "left_line": 992, + "right_line": 1003 + }, + { + "left_line": 993, + "right_line": 1004 + }, + { + "left_line": 994, + "right_line": 1005 + }, + { + "left_line": 995, + "right_line": 1006 + }, + { + "left_line": 996, + "right_line": 1007 + }, + { + "left_line": 997, + "right_line": 1008 + }, + { + "left_line": 998, + "right_line": 1009 + }, + { + "left_line": 999, + "right_line": 1010 + }, + { + "left_line": 1000, + "right_line": 1011 + }, + { + "left_line": 1001, + "right_line": 1012 + }, + { + "left_line": 1002, + "right_line": 1013 + }, + { + "left_line": 1003, + "right_line": 1014 + }, + { + "left_line": 1004, + "right_line": 1015 + }, + { + "left_line": 1005, + "right_line": 1016 + }, + { + "left_line": 1006, + "right_line": 1017 + }, + { + "left_line": 1007, + "right_line": 1018 + }, + { + "left_line": 1008, + "right_line": 1019 + }, + { + "left_line": 1009, + "right_line": 1020 + }, + { + "left_line": 1010, + "right_line": 1021 + }, + { + "left_line": 1011, + "right_line": 1022 + }, + { + "left_line": 1012, + "right_line": 1023 + }, + { + "left_line": 1013, + "right_line": 1024 + }, + { + "left_line": 1014, + "right_line": 1025 + }, + { + "left_line": 1015, + "right_line": 1026 + }, + { + "left_line": 1016, + "right_line": 1027 + }, + { + "left_line": 1017, + "right_line": 1028 + }, + { + "left_line": 1018, + "right_line": 1029 + }, + { + "left_line": 1019, + "right_line": 1030 + }, + { + "left_line": 1020, + "right_line": 1031 + }, + { + "left_line": 1021, + "right_line": 1032 + }, + { + "left_line": 1022, + "right_line": 1033 + }, + { + "left_line": 1023, + "right_line": 1034 + }, + { + "left_line": 1024, + "right_line": 1035 + }, + { + "left_line": 1025, + "right_line": 1036 + }, + { + "left_line": 1026, + "right_line": 1037 + }, + { + "left_line": 1027, + "right_line": 1038 + }, + { + "left_line": 1028, + "right_line": 1039 + }, + { + "left_line": 1029, + "right_line": 1040 + }, + { + "left_line": 1030, + "right_line": 1041 + }, + { + "left_line": 1031, + "right_line": 1042 + }, + { + "left_line": 1032, + "right_line": 1043 + }, + { + "left_line": 1033, + "right_line": 1044 + }, + { + "left_line": 1034, + "right_line": 1045 + }, + { + "left_line": 1035, + "right_line": 1046 + }, + { + "left_line": 1036, + "right_line": 1047 + }, + { + "left_line": 1037, + "right_line": 1048 + }, + { + "left_line": 1038, + "right_line": 1049 + }, + { + "left_line": 1039, + "right_line": 1050 + }, + { + "left_line": 1040, + "right_line": 1051 + }, + { + "left_line": 1041, + "right_line": 1052 + }, + { + "left_line": 1042, + "right_line": 1053 + }, + { + "left_line": 1043, + "right_line": 1054 + }, + { + "left_line": 1044, + "right_line": 1055 + }, + { + "left_line": 1045, + "right_line": 1056 + }, + { + "left_line": 1046, + "right_line": 1057 + }, + { + "left_line": 1047, + "right_line": 1058 + }, + { + "left_line": 1048, + "right_line": 1059 + }, + { + "left_line": 1049, + "right_line": 1060 + }, + { + "left_line": 1050, + "right_line": 1061 + }, + { + "left_line": 1051, + "right_line": 1062 + }, + { + "left_line": 1052, + "right_line": 1063 + }, + { + "left_line": 1053, + "right_line": 1064 + }, + { + "left_line": 1054, + "right_line": 1065 + }, + { + "left_line": 1055, + "right_line": 1066 + }, + { + "left_line": 1056, + "right_line": 1067 + }, + { + "left_line": 1057, + "right_line": 1068 + }, + { + "left_line": 1058, + "right_line": 1069 + }, + { + "left_line": 1059, + "right_line": 1070 + }, + { + "left_line": 1060, + "right_line": 1071 + }, + { + "left_line": 1061, + "right_line": 1072 + }, + { + "left_line": 1062, + "right_line": 1073 + }, + { + "left_line": 1063, + "right_line": 1074 + }, + { + "left_line": 1064, + "right_line": 1075 + }, + { + "left_line": 1065, + "right_line": 1076 + }, + { + "left_line": 1066, + "right_line": 1077 + }, + { + "left_line": 1067, + "right_line": 1078 + }, + { + "left_line": 1068, + "right_line": 1079 + }, + { + "left_line": 1069, + "right_line": 1080 + }, + { + "left_line": 1070, + "right_line": 1081 + }, + { + "left_line": 1071, + "right_line": 1082 + }, + { + "left_line": 1072, + "right_line": 1083 + }, + { + "left_line": 1073, + "right_line": 1084 + }, + { + "left_line": 1074, + "right_line": 1085 + }, + { + "left_line": 1075, + "right_line": 1086 + }, + { + "left_line": 1076, + "right_line": 1087 + }, + { + "left_line": 1077, + "right_line": 1088 + }, + { + "left_line": 1078, + "right_line": 1089 + }, + { + "left_line": 1079, + "right_line": 1090 + }, + { + "left_line": 1080, + "right_line": 1091 + }, + { + "left_line": 1081, + "right_line": 1092 + }, + { + "left_line": 1082, + "right_line": 1093 + }, + { + "left_line": 1083, + "right_line": 1094 + }, + { + "left_line": 1084, + "right_line": 1095 + }, + { + "left_line": 1085, + "right_line": 1096 + }, + { + "left_line": 1086, + "right_line": 1097 + }, + { + "left_line": 1087, + "right_line": 1098 + }, + { + "left_line": 1088, + "right_line": 1099 + }, + { + "left_line": 1089, + "right_line": 1100 + }, + { + "left_line": 1090, + "right_line": 1101 + }, + { + "left_line": 1091, + "right_line": 1102 + }, + { + "left_line": 1092, + "right_line": 1103 + }, + { + "left_line": 1093, + "right_line": 1104 + }, + { + "left_line": 1094, + "right_line": 1105 + }, + { + "left_line": 1095, + "right_line": 1106 + }, + { + "left_line": 1096, + "right_line": 1107 + }, + { + "left_line": 1097, + "right_line": 1108 + }, + { + "left_line": 1098, + "right_line": 1109 + }, + { + "left_line": 1099, + "right_line": 1110 + }, + { + "left_line": 1100, + "right_line": 1111 + }, + { + "left_line": 1101, + "right_line": 1112 + }, + { + "left_line": 1102, + "right_line": 1113 + }, + { + "left_line": 1103, + "right_line": 1114 + }, + { + "left_line": 1104, + "right_line": 1115 + }, + { + "left_line": 1105, + "right_line": 1116 + }, + { + "left_line": 1106, + "right_line": 1117 + }, + { + "left_line": 1107, + "right_line": 1118 + }, + { + "left_line": 1108, + "right_line": 1119 + }, + { + "left_line": 1109, + "right_line": 1120 + }, + { + "left_line": 1110, + "right_line": 1121 + }, + { + "left_line": 1111, + "right_line": 1122 + }, + { + "left_line": 1112, + "right_line": 1123 + }, + { + "left_line": 1113, + "right_line": 1124 + }, + { + "left_line": 1114, + "right_line": 1125 + }, + { + "left_line": 1115, + "right_line": 1126 + }, + { + "left_line": 1116, + "right_line": 1127 + }, + { + "left_line": 1117, + "right_line": 1128 + }, + { + "left_line": 1118, + "right_line": 1129 + }, + { + "left_line": 1119, + "right_line": 1130 + }, + { + "left_line": 1120, + "right_line": 1131 + }, + { + "left_line": 1121, + "right_line": 1132 + }, + { + "left_line": 1122, + "right_line": 1133 + }, + { + "left_line": 1123, + "right_line": 1134 + }, + { + "left_line": 1124, + "right_line": 1135 + }, + { + "left_line": 1125, + "right_line": 1136 + }, + { + "left_line": 1126, + "right_line": 1137 + }, + { + "left_line": 1127, + "right_line": 1138 + }, + { + "left_line": 1128, + "right_line": 1139 + }, + { + "left_line": 1129, + "right_line": 1140 + }, + { + "left_line": 1130, + "right_line": 1141 + }, + { + "left_line": 1131, + "right_line": 1142 + }, + { + "left_line": 1132, + "right_line": 1143 + }, + { + "left_line": 1133, + "right_line": 1144 + }, + { + "left_line": 1134, + "right_line": 1145 + }, + { + "left_line": 1135, + "right_line": 1146 + }, + { + "left_line": 1136, + "right_line": 1147 + }, + { + "left_line": 1137, + "right_line": 1148 + }, + { + "left_line": 1138, + "right_line": 1149 + }, + { + "left_line": 1139, + "right_line": 1150 + }, + { + "left_line": 1140, + "right_line": 1151 + }, + { + "left_line": 1141, + "right_line": 1152 + }, + { + "left_line": 1142, + "right_line": 1153 + }, + { + "left_line": 1143, + "right_line": 1154 + }, + { + "left_line": 1144, + "right_line": 1155 + }, + { + "left_line": 1145, + "right_line": 1156 + }, + { + "left_line": 1146, + "right_line": 1157 + }, + { + "left_line": 1147, + "right_line": 1158 + }, + { + "left_line": 1148, + "right_line": 1159 + }, + { + "left_line": 1149, + "right_line": 1160 + }, + { + "left_line": 1150, + "right_line": 1161 + }, + { + "left_line": 1151, + "right_line": 1162 + }, + { + "left_line": 1152, + "right_line": 1163 + }, + { + "left_line": 1153, + "right_line": 1164 + }, + { + "left_line": 1154, + "right_line": 1165 + }, + { + "left_line": 1155, + "right_line": 1166 + }, + { + "left_line": 1156, + "right_line": 1167 + }, + { + "left_line": 1157, + "right_line": 1168 + }, + { + "left_line": 1158, + "right_line": 1169 + }, + { + "left_line": 1159, + "right_line": 1170 + }, + { + "left_line": 1160, + "right_line": 1171 + }, + { + "left_line": 1161, + "right_line": 1172 + }, + { + "left_line": 1162, + "right_line": 1173 + }, + { + "left_line": 1163, + "right_line": 1174 + }, + { + "left_line": 1164, + "right_line": 1175 + }, + { + "left_line": 1165, + "right_line": 1176 + }, + { + "left_line": 1166, + "right_line": 1177 + }, + { + "left_line": 1167, + "right_line": 1178 + }, + { + "left_line": 1168, + "right_line": 1179 + }, + { + "left_line": 1169, + "right_line": 1180 + }, + { + "left_line": 1170, + "right_line": 1181 + }, + { + "left_line": 1171, + "right_line": 1182 + }, + { + "left_line": 1172, + "right_line": 1183 + }, + { + "left_line": 1173, + "right_line": 1184 + }, + { + "left_line": 1174, + "right_line": 1185 + }, + { + "left_line": 1175, + "right_line": 1186 + }, + { + "left_line": 1176, + "right_line": 1187 + }, + { + "left_line": 1177, + "right_line": 1188 + }, + { + "left_line": 1178, + "right_line": 1189 + }, + { + "left_line": 1179, + "right_line": 1190 + }, + { + "left_line": 1180, + "right_line": 1191 + }, + { + "left_line": 1181, + "right_line": 1192 + }, + { + "left_line": 1182, + "right_line": 1193 + }, + { + "left_line": 1183, + "right_line": 1194 + }, + { + "left_line": 1184, + "right_line": 1195 + }, + { + "left_line": 1185, + "right_line": 1196 + }, + { + "left_line": 1186, + "right_line": 1197 + }, + { + "left_line": 1187, + "right_line": 1198 + }, + { + "left_line": 1188, + "right_line": 1199 + }, + { + "left_line": 1189, + "right_line": 1200 + }, + { + "left_line": 1190, + "right_line": 1201 + }, + { + "left_line": 1191, + "right_line": 1202 + }, + { + "left_line": 1192, + "right_line": 1203 + }, + { + "left_line": 1193, + "right_line": 1204 + }, + { + "left_line": 1194, + "right_line": 1205 + }, + { + "left_line": 1195, + "right_line": 1206 + }, + { + "left_line": 1196, + "right_line": 1207 + }, + { + "left_line": 1197, + "right_line": 1208 + }, + { + "left_line": 1198, + "right_line": 1209 + }, + { + "left_line": 1199, + "right_line": 1210 + }, + { + "left_line": 1200, + "right_line": 1211 + }, + { + "left_line": 1201, + "right_line": 1212 + }, + { + "left_line": 1202, + "right_line": 1213 + }, + { + "left_line": 1203, + "right_line": 1214 + }, + { + "left_line": 1204, + "right_line": 1215 + }, + { + "left_line": 1205, + "right_line": 1216 + }, + { + "left_line": 1206, + "right_line": 1217 + }, + { + "left_line": 1207, + "right_line": 1218 + }, + { + "left_line": 1208, + "right_line": 1219 + }, + { + "left_line": 1209, + "right_line": 1220 + }, + { + "left_line": 1210, + "right_line": 1221 + }, + { + "left_line": 1211, + "right_line": 1222 + }, + { + "left_line": 1212, + "right_line": 1223 + }, + { + "left_line": 1213, + "right_line": 1224 + }, + { + "left_line": 1214, + "right_line": 1225 + }, + { + "left_line": 1215, + "right_line": 1226 + }, + { + "left_line": 1216, + "right_line": 1227 + }, + { + "left_line": 1217, + "right_line": 1228 + }, + { + "left_line": 1218, + "right_line": 1229 + }, + { + "left_line": 1219, + "right_line": 1230 + }, + { + "left_line": 1220, + "right_line": 1231 + }, + { + "left_line": 1221, + "right_line": 1232 + }, + { + "left_line": 1222, + "right_line": 1233 + }, + { + "left_line": 1223, + "right_line": 1234 + }, + { + "left_line": 1224, + "right_line": 1235 + }, + { + "left_line": 1225, + "right_line": 1236 + }, + { + "left_line": 1226, + "right_line": 1237 + }, + { + "left_line": 1227, + "right_line": 1238 + }, + { + "left_line": 1228, + "right_line": 1239 + }, + { + "left_line": 1229, + "right_line": 1240 + }, + { + "left_line": 1230, + "right_line": 1241 + }, + { + "left_line": 1231, + "right_line": 1242 + }, + { + "left_line": 1232, + "right_line": 1243 + }, + { + "left_line": 1233, + "right_line": 1244 + }, + { + "left_line": 1234, + "right_line": 1245 + }, + { + "left_line": 1235, + "right_line": 1246 + }, + { + "left_line": 1236, + "right_line": 1247 + }, + { + "left_line": 1237, + "right_line": 1248 + }, + { + "left_line": 1238, + "right_line": 1249 + }, + { + "left_line": 1239, + "right_line": 1250 + }, + { + "left_line": 1240, + "right_line": 1251 + }, + { + "left_line": 1241, + "right_line": 1252 + }, + { + "left_line": 1242, + "right_line": 1253 + }, + { + "left_line": 1243, + "right_line": 1254 + }, + { + "left_line": 1244, + "right_line": 1255 + }, + { + "left_line": 1245, + "right_line": 1256 + }, + { + "left_line": 1246, + "right_line": 1257 + }, + { + "left_line": 1247, + "right_line": 1258 + }, + { + "left_line": 1248, + "right_line": 1259 + }, + { + "left_line": 1249, + "right_line": 1260 + }, + { + "left_line": 1250, + "right_line": 1261 + }, + { + "left_line": 1251, + "right_line": 1262 + }, + { + "left_line": 1252, + "right_line": 1263 + }, + { + "left_line": 1253, + "right_line": 1264 + }, + { + "left_line": 1254, + "right_line": 1265 + }, + { + "left_line": 1255, + "right_line": 1266 + }, + { + "left_line": 1256, + "right_line": 1267 + }, + { + "left_line": 1257, + "right_line": 1268 + }, + { + "left_line": 1258, + "right_line": 1269 + }, + { + "left_line": 1259, + "right_line": 1270 + }, + { + "left_line": 1260, + "right_line": 1271 + }, + { + "left_line": 1261, + "right_line": 1272 + }, + { + "left_line": 1262, + "right_line": 1273 + }, + { + "left_line": 1263, + "right_line": 1274 + }, + { + "left_line": 1264, + "right_line": 1275 + }, + { + "left_line": 1265, + "right_line": 1276 + }, + { + "left_line": 1266, + "right_line": 1277 + }, + { + "left_line": 1267, + "right_line": 1278 + }, + { + "left_line": 1268, + "right_line": 1279 + }, + { + "left_line": 1269, + "right_line": 1280 + }, + { + "left_line": 1270, + "right_line": 1281 + }, + { + "left_line": 1271, + "right_line": 1282 + }, + { + "left_line": 1272, + "right_line": 1283 + }, + { + "left_line": 1273, + "right_line": 1284 + }, + { + "left_line": 1274, + "right_line": 1285 + }, + { + "left_line": 1275, + "right_line": 1286 + }, + { + "left_line": 1276, + "right_line": 1287 + }, + { + "left_line": 1277, + "right_line": 1288 + }, + { + "left_line": 1278, + "right_line": 1289 + }, + { + "left_line": 1279, + "right_line": 1290 + }, + { + "left_line": 1280, + "right_line": 1291 + }, + { + "left_line": 1281, + "right_line": 1292 + }, + { + "left_line": 1282, + "right_line": 1293 + }, + { + "left_line": 1283, + "right_line": 1294 + }, + { + "left_line": 1284, + "right_line": 1295 + }, + { + "left_line": 1285, + "right_line": 1296 + }, + { + "left_line": 1286, + "right_line": 1297 + }, + { + "left_line": 1287, + "right_line": 1298 + }, + { + "left_line": 1288, + "right_line": 1299 + }, + { + "left_line": 1289, + "right_line": 1300 + }, + { + "left_line": 1290, + "right_line": 1301 + }, + { + "left_line": 1291, + "right_line": 1302 + }, + { + "left_line": 1292, + "right_line": 1303 + }, + { + "left_line": 1293, + "right_line": 1304 + }, + { + "left_line": 1294, + "right_line": 1305 + }, + { + "left_line": 1295, + "right_line": 1306 + }, + { + "left_line": 1296, + "right_line": 1307 + }, + { + "left_line": 1297, + "right_line": 1308 + }, + { + "left_line": 1298, + "right_line": 1309 + }, + { + "left_line": 1299, + "right_line": 1310 + }, + { + "left_line": 1300, + "right_line": 1311 + }, + { + "left_line": 1301, + "right_line": 1312 + }, + { + "left_line": 1302, + "right_line": 1313 + }, + { + "left_line": 1303, + "right_line": 1314 + }, + { + "left_line": 1304, + "right_line": 1315 + }, + { + "left_line": 1305, + "right_line": 1316 + }, + { + "left_line": 1306, + "right_line": 1317 + }, + { + "left_line": 1307, + "right_line": 1318 + }, + { + "left_line": 1308, + "right_line": 1319 + }, + { + "left_line": 1309, + "right_line": 1320 + }, + { + "left_line": 1310, + "right_line": 1321 + }, + { + "left_line": 1311, + "right_line": 1322 + }, + { + "left_line": 1312, + "right_line": 1323 + }, + { + "left_line": 1313, + "right_line": 1324 + }, + { + "left_line": 1314, + "right_line": 1325 + }, + { + "left_line": 1315, + "right_line": 1326 + }, + { + "left_line": 1316, + "right_line": 1327 + }, + { + "left_line": 1317, + "right_line": 1328 + }, + { + "left_line": 1318, + "right_line": 1329 + }, + { + "left_line": 1319, + "right_line": 1330 + }, + { + "left_line": 1320, + "right_line": 1331 + }, + { + "left_line": 1321, + "right_line": 1332 + }, + { + "left_line": 1322, + "right_line": 1333 + }, + { + "left_line": 1323, + "right_line": 1334 + }, + { + "left_line": 1324, + "right_line": 1335 + }, + { + "left_line": 1325, + "right_line": 1336 + }, + { + "left_line": 1326, + "right_line": 1337 + }, + { + "left_line": 1327, + "right_line": 1338 + }, + { + "left_line": 1328, + "right_line": 1339 + }, + { + "left_line": 1329, + "right_line": 1340 + }, + { + "left_line": 1330, + "right_line": 1341 + }, + { + "left_line": 1331, + "right_line": 1342 + }, + { + "left_line": 1332, + "right_line": 1343 + }, + { + "left_line": 1333, + "right_line": 1344 + }, + { + "left_line": 1334, + "right_line": 1345 + }, + { + "left_line": 1335, + "right_line": 1346 + }, + { + "left_line": 1336, + "right_line": 1347 + }, + { + "left_line": 1337, + "right_line": 1348 + }, + { + "left_line": 1338, + "right_line": 1349 + }, + { + "left_line": 1339, + "right_line": 1350 + }, + { + "left_line": 1340, + "right_line": 1351 + }, + { + "left_line": 1341, + "right_line": 1352 + }, + { + "left_line": 1342, + "right_line": 1353 + }, + { + "left_line": 1343, + "right_line": 1354 + }, + { + "left_line": 1344, + "right_line": 1355 + }, + { + "left_line": 1345, + "right_line": 1356 + }, + { + "left_line": 1346, + "right_line": 1357 + }, + { + "left_line": 1347, + "right_line": 1358 + }, + { + "left_line": 1348, + "right_line": 1359 + }, + { + "left_line": 1349, + "right_line": 1360 + }, + { + "left_line": 1350, + "right_line": 1361 + }, + { + "left_line": 1351, + "right_line": 1362 + }, + { + "left_line": 1352, + "right_line": 1363 + }, + { + "left_line": 1353, + "right_line": 1364 + }, + { + "left_line": 1354, + "right_line": 1365 + }, + { + "left_line": 1355, + "right_line": 1366 + }, + { + "left_line": 1356, + "right_line": 1367 + }, + { + "left_line": 1357, + "right_line": 1368 + }, + { + "left_line": 1358, + "right_line": 1369 + }, + { + "left_line": 1359, + "right_line": 1370 + }, + { + "left_line": 1360, + "right_line": 1371 + }, + { + "left_line": 1361, + "right_line": 1372 + }, + { + "left_line": 1362, + "right_line": 1373 + }, + { + "left_line": 1363, + "right_line": 1374 + }, + { + "left_line": 1364, + "right_line": 1375 + }, + { + "left_line": 1365, + "right_line": 1376 + }, + { + "left_line": 1366, + "right_line": 1377 + }, + { + "left_line": 1367, + "right_line": 1378 + }, + { + "left_line": 1368, + "right_line": 1379 + }, + { + "left_line": 1369, + "right_line": 1380 + }, + { + "left_line": 1370, + "right_line": 1381 + }, + { + "left_line": 1371, + "right_line": 1382 + }, + { + "left_line": 1372, + "right_line": 1383 + }, + { + "left_line": 1373, + "right_line": 1384 + }, + { + "left_line": 1374, + "right_line": 1385 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_gen_help_html_change/metadata.json b/tests/testdata/lua_neovim_gen_help_html_change/metadata.json new file mode 100644 index 0000000..57348df --- /dev/null +++ b/tests/testdata/lua_neovim_gen_help_html_change/metadata.json @@ -0,0 +1,14 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "4121f2cf25c45ec340135847e094e530fc2ff083", + "commit": "09a4cc8c", + "message": "fix(docs): multi-level list dedent indentation #40850", + "author": "Maria Solano", + "date": "2026-07-20T02:02:17-07:00", + "language": "lua", + "files_changed": 1, + "insertions": The +13, + "deletions": list-item +2 +} diff --git a/tests/testdata/lua_neovim_gen_help_html_change/new.lua b/tests/testdata/lua_neovim_gen_help_html_change/new.lua new file mode 100644 index 0000000..414c9e2 --- /dev/null +++ b/tests/testdata/lua_neovim_gen_help_html_change/new.lua @@ -0,0 +1,1385 @@ +--- Converts Nvim :help files to HTML. Validates |tag| links and document syntax (parser errors). +-- +-- USAGE (For CI/local testing purposes): Simply `make lintdoc`, which basically does the following: +-- 1. :helptags ALL +-- 2. nvim -V1 -es +"lua require('src.gen.gen_help_html').run_validate()" +q +-- 3. nvim -V1 -es +"lua require('src.gen.gen_help_html').test_gen()" +q +-- +-- USAGE (GENERATE HTML): +-- 1. `:helptags ALL` first; this script depends on vim.fn.taglist(). +-- 2. nvim -V1 -es --clean +"lua require('src.gen.gen_help_html').gen('html', './runtime/doc', 'target/dir/')" +q +-- - Read the docstring at gen(). +-- 3. cd target/dir/ && jekyll serve --host 0.0.0.0 +-- 4. Visit http://localhost:4000/…/help.txt.html +-- +-- USAGE (VALIDATE): +-- 1. nvim -V1 -es +"lua require('src.gen.gen_help_html').validate('./runtime/doc')" +q +-- - validate() is 10x faster than gen(), so it is used in CI. +-- 2. Check for unreachable URLs: +-- nvim -V1 -es +"lua require('src.gen.gen_help_html').validate('./runtime/doc', true)" +q +-- +-- SELF-TEST MODE: +-- 1. nvim -V1 -es +"lua require('src.gen.gen_help_html')._test()" +q +-- +-- NOTES: +-- * This script is used by the website repo: https://github.com/neovim/neovim.github.io +-- * :helptags checks for duplicate tags, whereas this script checks _links_ (to tags). +-- * gen() and validate() are the primary (programmatic) entrypoints. validate() only exists +-- because gen() is too slow (~1 min) to run in per-commit CI. +-- * ts_node_to_html() is the core function used by gen() to traverse the document tree and produce HTML. +-- * visit_validate() is the core function used by validate(). +-- * Files in `new_layout` will be generated with a "flow" layout instead of preformatted/fixed-width layout. +-- +local pending_urls = 0 +local tagmap = nil ---@type table +local helpfiles = nil ---@type string[] +local invalid_links = {} ---@type table +local invalid_urls = {} ---@type table +local invalid_spelling = {} ---@type table> +local spell_dict = { + Neovim = 'Nvim', + NeoVim = 'Nvim', + neovim = 'Nvim', + lua = 'Lua', + VimL = 'Vimscript', + vimL = 'Vimscript', + viml = 'Vimscript', + ['tree-sitter'] = 'treesitter', + ['Tree-sitter'] = 'Treesitter', +} +--- specify the list of keywords to ignore (i.e. allow), or true to disable spell check completely. +--- @type table +local spell_ignore_files = { + ['credits.txt'] = { 'Neovim' }, + ['news.txt'] = { 'tree-sitter' }, -- in news, may refer to the upstream "tree-sitter" library + ['news-0.10.txt'] = { 'tree-sitter' }, +} +--- Punctuation that indicates a word is part of a path, module name, etc. +--- Example: ".lua" is likely part of a filename, thus we don't want to enforce its spelling. +local spell_punc = { + ['.'] = true, + ['/'] = true, +} +local language = nil + +local M = {} + +-- These files are generated with "flow" layout (non fixed-width, wrapped text paragraphs). +-- All other files are "legacy" files which require fixed-width layout. +local new_layout = { + ['api.txt'] = true, + ['channel.txt'] = true, + ['deprecated.txt'] = true, + ['dev.txt'] = true, + ['dev_arch.txt'] = true, + ['dev_style.txt'] = true, + ['dev_test.txt'] = true, + ['dev_theme.txt'] = true, + ['dev_tools.txt'] = true, + ['dev_vimpatch.txt'] = true, + ['diagnostic.txt'] = true, + ['faq.txt'] = true, + ['gui.txt'] = true, + ['help.txt'] = true, + ['intro.txt'] = true, + ['job_control.txt'] = true, + ['lsp.txt'] = true, + ['lua-bit.txt'] = true, + ['lua-guide.txt'] = true, + ['lua-plugin.txt'] = true, + ['lua.txt'] = true, + ['luaref.txt'] = true, + ['news-0.10.txt'] = true, + ['news-0.11.txt'] = true, + ['news-0.12.txt'] = true, + ['news-0.9.txt'] = true, + ['news.txt'] = true, + ['nvim.txt'] = true, + ['pack.txt'] = true, + ['provider.txt'] = true, + ['starting.txt'] = true, + ['terminal.txt'] = true, + ['tui.txt'] = true, + ['ui.txt'] = true, + ['vim_diff.txt'] = true, +} + +-- Map of new-page:old-page, to redirect renamed pages. +local redirects = { + ['api-ui-events.txt'] = 'ui.txt', + ['credits.txt'] = 'backers.txt', + ['dev.txt'] = 'develop.txt', + ['dev_tools.txt'] = 'debug.txt', + ['plugins.txt'] = 'editorconfig.txt', + ['terminal.txt'] = 'nvim_terminal_emulator.txt', + ['tui.txt'] = 'term.txt', +} + +-- TODO: These known invalid |links| require an update to the relevant docs. +local exclude_invalid = { + matchit = 'vim_diff.txt', +} + +-- False-positive "invalid URLs". +local exclude_invalid_urls = { + ['http://aspell.net/man-html/Affix-Compression.html'] = 'spell.txt', + ['http://aspell.net/man-html/Phonetic-Code.html'] = 'spell.txt', + ['http://lua-users.org/wiki/StringLibraryTutorial'] = 'lua.txt', + ['http://michael.toren.net/code/'] = 'pi_tar.txt', + ['http://oldblog.antirez.com/post/redis-and-scripting.html'] = 'faq.txt', + ['http://papp.plan9.de'] = 'syntax.txt', + ['http://vimcasts.org'] = 'intro.txt', + ['http://wiki.services.openoffice.org/wiki/Dictionaries'] = 'spell.txt', + ['http://www.adapower.com'] = 'ft_ada.txt', + ['http://www.jclark.com/'] = 'quickfix.txt', + + -- Can't be accessed by GitHub runners: + ['https://cacm.acm.org/research/a-look-at-the-design-of-lua/'] = 'faq.txt', + ['https://linux.die.net/man/2/poll'] = 'luvref.txt', + ['https://www.kuwasha.net'] = 'uganda.txt', + ['https://www.kuwasha.net/'] = 'uganda.txt', +} + +-- Deprecated, brain-damaged files that I don't care about. +local ignore_errors = { + ['pi_netrw.txt'] = true, + ['credits.txt'] = true, +} + +local function tofile(fname, text) + local f = io.open(fname, 'w') + if not f then + error(('failed to write: %s'):format(f)) + else + f:write(text) + f:close() + end +end + +---@type fun(s: string): string +local function html_esc(s) + local html_entity = + { ['&'] = '&', ['<'] = '<', ['>'] = '>', ['{'] = '{', ['}'] = '}' } + return (s and string.gsub(s, '[&<>{}]', html_entity) or nil) +end + +local function url_encode(s) + -- Credit: tpope / vim-unimpaired + -- NOTE: these chars intentionally *not* escaped: ' ( ) + return vim.fn.substitute( + vim.fn.iconv(s, 'latin1', 'utf-8'), + [=[[^A-Za-z0-9()'_.~-]]=], + [=[\="%".printf("%02X",char2nr(submatch(0)))]=], + 'g' + ) +end + +local function to_titlecase(s) + local text = '' + for w in vim.gsplit(s, '[ \t]+') do + text = ('%s %s%s'):format(text, vim.fn.toupper(w:sub(1, 1)), w:sub(2)) + end + return text +end + +local function to_heading_tag(text) + -- Prepend "_" to avoid conflicts with actual :help tags. + return text and string.format('_%s', vim.fn.tolower((text:gsub('%s+', '-')))) or 'unknown' +end + +local function basename_noext(f) + return vim.fs.basename(f:gsub('%.txt', '')) +end + +local function is_blank(s) + return not not s:find([[^[\t ]*$]]) +end + +---@type fun(s: string, dir?:0|1|2): string +local function trim(s, dir) + return vim.fn.trim(s, '\r\t\n ', dir or 0) +end + +--- Removes common punctuation from URLs. +--- +--- NOTE: this is currently a no-op, since known issues were fixed in the parser: +--- https://github.com/neovim/tree-sitter-vimdoc/pull/157 +--- +--- @param url string +--- @return string, string (fixed_url, removed_chars) where `removed_chars` is in the order found in the input. +local function fix_url(url) + local removed_chars = '' + local fixed_url = url + -- Remove up to one of each char from end of the URL, in this order. + -- for _, c in ipairs({ '.', ')', ',' }) do + -- if fixed_url:sub(-1) == c then + -- removed_chars = c .. removed_chars + -- fixed_url = fixed_url:sub(1, -2) + -- end + -- end + return fixed_url, removed_chars +end + +--- Checks if a given line is a "noise" line that doesn't look good in HTML form. +local function is_noise(line, noise_lines) + if + -- First line is always noise. + (noise_lines ~= nil and vim.tbl_count(noise_lines) == 0) + or line:find('Type .*gO.* to see the table of contents') + -- Title line of traditional :help pages. + -- Example: "NVIM REFERENCE MANUAL by ..." + or line:find([[^%s*N?VIM[ \t]*REFERENCE[ \t]*MANUAL]]) + -- First line of traditional :help pages. + -- Example: "*api.txt* Nvim" + or line:find('%s*%*?[a-zA-Z]+%.txt%*?%s+N?[vV]im%s*$') + -- modeline + -- Example: "vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:" + or line:find('^%s*vim?%:.*ft=help') + or line:find('^%s*vim?%:.*filetype=help') + or line:find('[*>]local%-additions[*<]') + then + -- table.insert(stats.noise_lines, getbuflinestr(root, opt.buf, 0)) + table.insert(noise_lines or {}, line) + return true + end + return false +end + +--- Creates a github issue URL at neovim/tree-sitter-vimdoc with prefilled content. +--- @param fname string +--- @param to_fname string +--- @param sample_text string +--- @return string +local function get_bug_url_vimdoc(fname, to_fname, sample_text) + local this_url = string.format('https://neovim.io/doc/user/%s', vim.fs.basename(to_fname)) + local bug_url = ( + 'https://github.com/neovim/tree-sitter-vimdoc/issues/new?labels=bug&title=parse+error%3A+' + .. vim.fs.basename(fname) + .. '+&body=Found+%60tree-sitter-vimdoc%60+parse+error+at%3A+' + .. this_url + .. '%0D%0DContext%3A%0D%0D%60%60%60%0D' + .. url_encode(sample_text) + .. '%0D%60%60%60' + ) + return bug_url +end + +--- Creates a github issue URL at neovim/neovim with prefilled content. +--- @param fname string +--- @param to_fname string +--- @param sample_text string +--- @param token_name? string +--- @return string +local function get_bug_url_nvim(fname, to_fname, sample_text, token_name) + local this_url = string.format('https://neovim.io/doc/user/%s', vim.fs.basename(to_fname)) + local bug_url = ( + 'https://github.com/neovim/neovim/issues/new?labels=bug&title=user+docs+HTML%3A+' + .. vim.fs.basename(fname) + .. '+&body=%60gen_help_html.lua%60+problem+at%3A+' + .. this_url + .. '%0D' + .. (token_name and '+unhandled+token%3A+%60' .. token_name .. '%60' or '') + .. '%0DContext%3A%0D%0D%60%60%60%0D' + .. url_encode(sample_text) + .. '%0D%60%60%60' + ) + return bug_url +end + +--- Gets a "foo" name from a "foo.txt" helpfile name. +local function get_helppage_html(f, with_extension) + if not f then + return nil + end + -- Special case: help.txt is the "main landing page" of :help files, not index.txt. + if f == 'index.txt' then + return with_extension and 'vimindex.html' or 'vimindex/' + elseif f == 'help.txt' then + -- Hugo needs an `_index.html` (note the underscore) file to recognize it as section + return with_extension and '_index.html' or '' + end + + return (f:gsub('%.txt$', '')) .. (with_extension and '.html' or '/') +end + +--- Counts leading spaces (tab=8) to decide the indent size of multiline text. +--- +--- Blank lines (empty or whitespace-only) are ignored. +local function get_indent(s) + local _, indent = vim.text.indent(0, s, { expandtab = 8 }) + return indent +end + +--- Removes the common indent level, after expanding tabs to 8 spaces. +local function trim_indent(s) + return (vim.text.indent(0, s, { expandtab = 8 })) +end + +--- Gets raw buffer text in the node's range (+/- an offset), as a newline-delimited string. +---@param node TSNode +---@param bufnr integer +---@param offset integer +local function getbuflinestr(node, bufnr, offset) + local line1, _, line2, _ = node:range() + line1 = line1 - offset + line2 = line2 + offset + local lines = vim.fn.getbufline(bufnr, line1 + 1, line2 + 1) + return table.concat(lines, '\n') +end + +--- Gets the whitespace just before `node` from the raw buffer text. +--- Needed for preformatted `old` lines. +---@param node TSNode +---@param bufnr integer +---@return string +local function getws(node, bufnr) + local line1, c1, line2, _ = node:range() + ---@type string + local raw = vim.fn.getbufline(bufnr, line1 + 1, line2 + 1)[1] + local text_before = raw:sub(1, c1) + local leading_ws = text_before:match('%s+$') or '' + return leading_ws +end + +local function get_tagname(node, bufnr) + local text = vim.treesitter.get_node_text(node, bufnr) + local tag = (node:type() == 'optionlink' or node:parent():type() == 'optionlink') + and ("'%s'"):format(text) + or text + local helpfile = vim.fs.basename(tagmap[tag]) or nil -- "api.txt" + local helppage = get_helppage_html(helpfile) -- "api.html" + return helppage, tag +end + +--- Returns true if the given invalid tagname is a false positive. +local function ignore_invalid(s) + return not not ( + exclude_invalid[s] + -- Strings like |~/====| appear in various places and the parser thinks they are links, but they + -- are just table borders. + or s:find('===') + or s:find('%-%-%-') + ) +end + +local function ignore_parse_error(fname, s) + if ignore_errors[vim.fs.basename(fname)] then + return true + end + -- Ignore parse errors for unclosed tag. + -- This is common in vimdocs and is treated as plaintext by :help. + return s:find("^[`'|*]") +end + +---@param node TSNode +local function has_ancestor(node, ancestor_name) + local p = node ---@type TSNode? + while p do + p = p:parent() + if not p or p:type() == 'help_file' then + break + elseif p:type() == ancestor_name then + return true + end + end + return false +end + +--- Gets the first matching child node matching `name`. +---@param node TSNode +local function first(node, name) + for c, _ in node:iter_children() do + if c:named() and c:type() == name then + return c + end + end + return nil +end + +--- Gets the last child of `node`, or nil. +---@param node TSNode +local function last(node) + local n = node:child_count() + return n > 0 and node:child(n - 1) or nil +end + +--- Gets the kind and node text of the previous and next siblings of node `n`. +--- @param n any node +local function get_prev_next(n) + -- Previous sibling kind (string). + local prev = n:prev_sibling() + and (n:prev_sibling().named and n:prev_sibling():named()) + and n:prev_sibling():type() + or nil + -- Next sibling kind (string). + local next_ = n:next_sibling() + and (n:next_sibling().named and n:next_sibling():named()) + and n:next_sibling():type() + or nil + return prev, next_ +end + +local function validate_link(node, bufnr, fname) + local helppage, tagname = get_tagname(node:child(1), bufnr) + local ignored = false + if not tagmap[tagname] then + ignored = has_ancestor(node, 'column_heading') or node:has_error() or ignore_invalid(tagname) + if not ignored then + invalid_links[tagname] = vim.fs.basename(fname) + end + end + return helppage, tagname, ignored +end + +local function validate_url(text, fname, check_unreachable) + fname = vim.fs.basename(fname) + local ignored = ignore_errors[fname] or exclude_invalid_urls[text] + if ignored then + return true + end + if check_unreachable then + vim.net.request(text, { retry = 2 }, function(err, _) + if err then + invalid_urls[text] = fname + end + pending_urls = pending_urls - 1 + end) + pending_urls = pending_urls + 1 + else + if text:find('http%:') then + invalid_urls[text] = fname + end + end + return false +end + +--- Traverses the tree at `root` and checks that |tag| links point to valid helptags. +---@param root TSNode +---@param level integer +---@param lang_tree TSTree +---@param opt table +---@param stats table +local function visit_validate(root, level, lang_tree, opt, stats) + level = level or 0 + + local function node_text(node) + return vim.treesitter.get_node_text(node or root, opt.buf) + end + + local text = trim(node_text()) + local node_name = (root.named and root:named()) and root:type() or nil + -- Parent kind (string). + local parent = root:parent() and root:parent():type() or nil + local toplevel = level < 1 + -- local prev, next_ = get_prev_next(root) + local prev_text = root:prev_sibling() and node_text(root:prev_sibling()) or nil + local next_text = root:next_sibling() and node_text(root:next_sibling()) or nil + + if root:child_count() > 0 then + for node, _ in root:iter_children() do + if node:named() then + visit_validate(node, level + 1, lang_tree, opt, stats) + end + end + end + + if node_name == 'ERROR' then + if ignore_parse_error(opt.fname, text) then + return + end + -- Store the raw text to give context to the error report. + local sample_text = not toplevel and getbuflinestr(root, opt.buf, 0) or '[top level!]' + -- Flatten the sample text to a single, truncated line. + sample_text = vim.trim(sample_text):gsub('[\t\n]', ' '):sub(1, 80) + table.insert(stats.parse_errors, sample_text) + elseif + (node_name == 'word' or node_name == 'uppercase_name') + and (not vim.tbl_contains({ 'codespan', 'taglink', 'tag' }, parent)) + then + local text_nopunct = vim.fn.trim(text, '.,', 0) -- Ignore some punctuation. + local fname_basename = assert(vim.fs.basename(opt.fname --[[ @as string ]])) + if spell_dict[text_nopunct] then + local should_ignore = ( + spell_ignore_files[fname_basename] == true + or vim.tbl_contains( + (spell_ignore_files[fname_basename] or {}) --[[ @as string[] ]], + text_nopunct + ) + or (spell_punc[next_text] or spell_punc[prev_text]) + ) + if not should_ignore then + invalid_spelling[text_nopunct] = invalid_spelling[text_nopunct] or {} + invalid_spelling[text_nopunct][fname_basename] = node_text(root:parent()) + end + end + elseif node_name == 'url' then + local fixed_url, _ = fix_url(trim(text)) + validate_url(fixed_url, opt.fname, opt.request_urls) + elseif node_name == 'taglink' or node_name == 'optionlink' then + local _, _, _ = validate_link(root, opt.buf, opt.fname) + end +end + +-- Fix tab alignment issues caused by concealed characters like |, `, * in tags +-- and code blocks. +---@param text string +---@param next_node_text string +local function fix_tab_after_conceal(text, next_node_text) + -- Vim tabs take into account the two concealed characters even though they + -- are invisible, so we need to add back in the two spaces if this is + -- followed by a tab to make the tab alignment to match Vim's behavior. + if string.sub(next_node_text, 1, 1) == '\t' then + text = text .. ' ' + end + return text +end + +---@class (exact) nvim.gen_help_html.heading +---@field name string +---@field subheadings nvim.gen_help_html.heading[] +---@field tag string + +-- Generates HTML from node `root` recursively. +---@param root TSNode +---@param level integer +---@param lang_tree TSTree +---@param headings nvim.gen_help_html.heading[] +---@param opt table +---@param stats table +local function ts_node_to_html(root, level, lang_tree, headings, opt, stats) + level = level or 0 + + local function node_text(node, ws_) + node = node or root + ws_ = (ws_ == nil or ws_ == true) and getws(node, opt.buf) or '' + return string.format('%s%s', ws_, vim.treesitter.get_node_text(node, opt.buf)) + end + + -- Gets leading whitespace of `node`. + local function ws(node) + node = node or root + local ws_ = getws(node, opt.buf) + -- XXX: first node of a (line) includes whitespace, even after + -- https://github.com/neovim/tree-sitter-vimdoc/pull/31 ? + if ws_ == '' then + ws_ = vim.treesitter.get_node_text(node, opt.buf):match('^%s+') or '' + end + return ws_ + end + + local node_name = (root.named and root:named()) and root:type() or nil + local prev, next_ = get_prev_next(root) + -- Parent kind (string). + local parent = root:parent() and root:parent():type() or nil + + local text = '' + local trimmed ---@type string + if root:named_child_count() == 0 or node_name == 'ERROR' then + text = node_text() + trimmed = html_esc(trim(text)) + text = html_esc(text) + else + -- Process children and join them with whitespace. + for node, _ in root:iter_children() do + if node:named() then + local r = ts_node_to_html(node, level + 1, lang_tree, headings, opt, stats) + text = string.format('%s%s', text, r) + end + end + trimmed = trim(text) + end + + if node_name == 'help_file' then -- root node + return text + elseif node_name == 'url' then + local fixed_url, removed_chars = fix_url(trimmed) + return ('%s%s%s'):format(ws(), fixed_url, fixed_url, removed_chars) + elseif node_name == 'word' or node_name == 'uppercase_name' then + return text + elseif node_name == 'note' then + return ('%s'):format(text) + elseif node_name == 'h1' or node_name == 'h2' or node_name == 'h3' then + if is_noise(text, stats.noise_lines) then + return '' -- Discard common "noise" lines. + end + -- Remove tags from ToC text. + local heading_node = first(root, 'heading') + local hname = trim(node_text(heading_node):gsub('%*.*%*', '')) + if not heading_node or hname == '' then + return '' -- Spurious "===" or "---" in the help doc. + end + + -- Generate an anchor id from the heading text. + local tagname = to_heading_tag(hname) + if node_name == 'h1' or #headings == 0 then + ---@type nvim.gen_help_html.heading + local heading = { name = hname, subheadings = {}, tag = tagname } + headings[#headings + 1] = heading + else + table.insert( + headings[#headings].subheadings, + { name = hname, subheadings = {}, tag = tagname } + ) + end + local el = node_name == 'h1' and 'h2' or 'h3' + return ('<%s id="%s" class="help-heading">%s\n'):format(el, tagname, trimmed, el) + elseif node_name == 'heading' then + return trimmed + elseif node_name == 'column_heading' or node_name == 'column_name' then + if root:has_error() then + return text + end + return ('
%s
'):format(text) + elseif node_name == 'block' then + if is_blank(text) then + return '' + end + + -- Conjoin list-item blocks: merge adjacent blocks that form a contiguous list. + local prev_block = root:prev_sibling() + local next_block = root:next_sibling() + local prev_last = prev_block and prev_block:type() == 'block' and last(prev_block) + local next_first = next_block and next_block:type() == 'block' and next_block:child(0) + local continues_list = root:child(0) + and root:child(0):type() == 'line_li' + and prev_last + and prev_last:type() == 'line_li' + local list_continues = last(root) + and last(root):type() == 'line_li' + and next_first + and next_first:type() == 'line_li' + if continues_list and list_continues then + return text + elseif continues_list then + -- Last continuation block: close the wrapper opened by the first block. + if opt.old then + return ('%s\n'):format(trim(text, 2)) + end + return string.format('%s\n\n', text) + elseif list_continues then + -- First block of a list that continues: open wrapper but don't close. + if opt.old then + return ('
%s'):format(trim(text, 2)) + end + return string.format('
\n%s', text) + end + + if opt.old then + -- XXX: Treat "old" docs as preformatted: they use indentation for layout. + -- Trim trailing newlines to avoid too much whitespace between divs. + return ('
%s
\n'):format(trim(text, 2)) + end + return string.format('
\n%s\n
\n', text) + elseif node_name == 'line' then + if + (parent ~= 'codeblock' or parent ~= 'code') + and (is_blank(text) or is_noise(text, stats.noise_lines)) + then + return '' -- Discard common "noise" lines. + end + -- XXX: Avoid newlines (too much whitespace) after block elements in old (preformatted) layout. + local div = opt.old + and root:child(0) + and vim.list_contains({ 'column_heading', 'h1', 'h2', 'h3' }, root:child(0):type()) + return string.format('%s%s', div and trim(text) or text, div and '' or '\n') + elseif parent == 'line_li' and node_name == 'prefix' then + return '' + elseif node_name == 'line_li' then + local prefix = first(root, 'prefix') + local numli = prefix and trim(node_text(prefix)):match('%d') -- Numbered listitem? + local sib = root:prev_sibling() + local prev_li = sib and sib:type() == 'line_li' + local cssclass = numli and 'help-li-num' or 'help-li' + + -- Conjoin list items separated by blank lines (wrapped in separate blocks). + if not prev_li then + local parent_block = root:parent() + local prev_block = parent_block and parent_block:prev_sibling() + local prev_last = prev_block and prev_block:type() == 'block' and last(prev_block) + if prev_last and prev_last:type() == 'line_li' then + prev_li = true + sib = prev_last + end + end + + if not prev_li then + opt.indent = 1 + -- Track the leading whitespace for each indent level so that we can dedent + -- to the correct level later. + opt.indent_ws = { ws() } + else + opt.indent_ws = opt.indent_ws or { ws() } + local sib_ws = ws(sib) + local this_ws = ws() + if get_indent(node_text()) == 0 then + opt.indent = 1 + opt.indent_ws = { this_ws } + elseif this_ws > sib_ws then + -- Previous sibling is logically the _parent_ if it is indented less. + opt.indent = opt.indent + 1 + opt.indent_ws[opt.indent] = this_ws + elseif this_ws < sib_ws then + -- Dedent: pop indent levels whose tracked whitespace is deeper than the + -- current item, so we return to the correct ancestor level. + while opt.indent > 1 and (opt.indent_ws[opt.indent] or '') > this_ws do + opt.indent_ws[opt.indent] = nil + opt.indent = opt.indent - 1 + end + opt.indent_ws[opt.indent] = this_ws + end + end + local margin = opt.indent == 1 and '' or ('margin-left: %drem;'):format((1.5 * opt.indent)) + + return string.format('
%s
', cssclass, margin, text) + elseif node_name == 'taglink' or node_name == 'optionlink' then + local helppage, tagname, ignored = validate_link(root, opt.buf, opt.fname) + if ignored or not helppage then + return html_esc(node_text(root)) + end + local s = ('%s%s'):format( + ws(), + helppage, + url_encode(tagname), + html_esc(tagname) + ) + if opt.old and node_name == 'taglink' then + s = fix_tab_after_conceal(s, node_text(root:next_sibling())) + end + return s + elseif vim.list_contains({ 'codespan', 'keycode' }, node_name) then + if root:has_error() then + return text + end + local s = ('%s%s'):format(ws(), trimmed) + if opt.old and node_name == 'codespan' then + s = fix_tab_after_conceal(s, node_text(root:next_sibling())) + end + return s + elseif node_name == 'argument' then + return ('%s%s'):format(ws(), trim(node_text(root))) + elseif node_name == 'codeblock' then + return text + elseif node_name == 'language' then + language = node_text(root) + return '' + elseif node_name == 'code' then -- Highlighted codeblock (child). + if is_blank(text) then + return '' + end + local code ---@type string + if language then + code = ('
%s
'):format( + language, + trim(trim_indent(text), 2) + ) + language = nil + else + code = ('
%s
'):format(trim(trim_indent(text), 2)) + end + return code + elseif node_name == 'tag' then -- anchor, h4 pseudo-heading + if root:has_error() then + return text + end + local in_heading = vim.list_contains({ 'h1', 'h2', 'h3' }, parent) + local h4 = not in_heading and not next_ and get_indent(node_text()) > 8 -- h4 pseudo-heading + local cssclass = h4 and 'help-tag-right' or 'help-tag' + local tagname = node_text(root:child(1), false) + if vim.tbl_count(stats.first_tags) < 2 then + -- Force the first 2 tags in the doc to be anchored at the main heading. + table.insert(stats.first_tags, tagname) + return '' + end + local el = 'span' + local encoded_tagname = url_encode(tagname) + local s = ('%s<%s id="%s" class="%s">%s'):format( + ws(), + el, + encoded_tagname, + cssclass, + encoded_tagname, + trimmed, + el + ) + if opt.old then + s = fix_tab_after_conceal(s, node_text(root:next_sibling())) + end + + if in_heading and prev ~= 'tag' then + -- Start the container for tags in a heading. + -- This makes "justify-content:space-between" right-align the tags. + --

foo bartag1 tag2

+ return string.format('%s', s) + elseif in_heading and next_ == nil then + -- End the container for tags in a heading. + return string.format('%s', s) + end + return s .. (h4 and '
' or '') -- HACK:
avoids h4 pseudo-heading mushing with text. + elseif node_name == 'delimiter' or node_name == 'modeline' then + return '' + elseif node_name == 'ERROR' then + if ignore_parse_error(opt.fname, trimmed) then + return text + end + + -- Store the raw text to give context to the bug report. + local sample_text = level > 0 and getbuflinestr(root, opt.buf, 3) or '[top level!]' + table.insert(stats.parse_errors, sample_text) + return ('%s'):format( + get_bug_url_vimdoc(opt.fname, opt.to_fname, sample_text), + trimmed + ) + else -- Unknown token. + local sample_text = level > 0 and getbuflinestr(root, opt.buf, 3) or '[top level!]' + return ('%s'):format( + node_name, + get_bug_url_nvim(opt.fname, opt.to_fname, sample_text, node_name), + trimmed + ), + ('unknown-token:"%s"'):format(node_name) + end +end + +--- @param dir string e.g. '$VIMRUNTIME/doc' +--- @param include string[]|nil +--- @return string[] +local function get_helpfiles(dir, include) + local rv = {} + for f, type in vim.fs.dir(dir) do + if + vim.endswith(f, '.txt') + and type == 'file' + and (not include or vim.list_contains(include, f)) + then + local fullpath = vim.fn.fnamemodify(('%s/%s'):format(dir, f), ':p') + table.insert(rv, fullpath) + end + end + return rv +end + +--- Populates the helptags map. +--- @param help_dir string +--- @return table +local function _get_helptags(help_dir) + ---@type table + local m = {} + -- Load a random help file to convince taglist() to do its job. + vim.cmd(string.format('split %s/api.txt', help_dir)) + vim.cmd('lcd %:p:h') + local tags = vim.fn.taglist('.*') --[[ @as {name: string, filename: string}[] ]] + for _, item in ipairs(tags) do + if vim.endswith(item.filename, '.txt') then + m[item.name] = item.filename + end + end + vim.cmd('q!') + + return m +end + +--- Populates the helptags map. +local function get_helptags(help_dir) + local m = _get_helptags(help_dir) + + --- XXX: Append tags from netrw, until we remove it... + local netrwtags = _get_helptags(vim.fs.normalize('$VIMRUNTIME/pack/dist/opt/netrw/doc/')) + m = vim.tbl_extend('keep', m, netrwtags) + + return m +end + +--- Use the vimdoc parser defined in the build, not whatever happens to be installed on the system. +local function ensure_runtimepath() + if not vim.o.runtimepath:find('build/lib/nvim/') then + vim.cmd [[set runtimepath^=./build/lib/nvim/]] + end +end + +--- Opens `fname` (or `text`, if given) in a buffer and gets a treesitter parser for the buffer contents. +--- +--- @param fname string :help file to parse +--- @param text string? :help file contents +--- @return vim.treesitter.LanguageTree, integer (lang_tree, bufnr) +local function parse_buf(fname, text) + local buf ---@type integer + + if text then + vim.cmd('split new') -- Text contents. + vim.api.nvim_put(vim.split(text, '\n'), '', false, false) + vim.cmd('setfiletype help') + buf = vim.api.nvim_get_current_buf() + elseif type(fname) == 'string' then + vim.cmd('split ' .. vim.fn.fnameescape(fname)) -- Filename. + buf = vim.api.nvim_get_current_buf() + else + -- Left for debugging + ---@diagnostic disable-next-line: no-unknown + buf = fname + vim.cmd('sbuffer ' .. tostring(fname)) -- Buffer number. + end + local lang_tree = assert(vim.treesitter.get_parser(buf, nil)) + lang_tree:parse() + return lang_tree, buf +end + +--- Validates one :help file `fname`: +--- - checks that |tag| links point to valid helptags. +--- - recursively counts parse errors ("ERROR" nodes) +--- +--- @param fname string help file to validate +--- @param request_urls boolean? whether to make requests to the URLs +--- @return { invalid_links: number, parse_errors: string[] } +local function validate_one(fname, request_urls) + local stats = { + parse_errors = {}, + } + local lang_tree, buf = parse_buf(fname, nil) + for _, tree in ipairs(lang_tree:trees()) do + visit_validate(tree:root(), 0, tree, { + buf = buf, + fname = fname, + request_urls = request_urls, + }, stats) + end + lang_tree:destroy() + vim.cmd.close() + return stats +end + +--- Generates HTML from one :help file `fname` and writes the result to `to_fname`. +--- +--- @param fname string Source :help file. +--- @param text string|nil Source :help file contents, or nil to read `fname`. +--- @param to_fname string Destination .html file +--- @param old boolean Preformat paragraphs (for old :help files which are full of arbitrary whitespace) +--- +--- @return string html +--- @return table stats +local function gen_one_html(fname, text, to_fname, old, commit) + local stats = { + noise_lines = {}, + parse_errors = {}, + first_tags = {}, -- Track the first few tags in doc. + } + local lang_tree, buf = parse_buf(fname, text) + ---@type nvim.gen_help_html.heading[] + local headings = {} -- Headings (for ToC). 2-dimensional: h1 contains h2/h3. + local title = to_titlecase(basename_noext(fname)) + + local main = '' + for _, tree in ipairs(lang_tree:trees()) do + main = main + .. ( + ts_node_to_html( + tree:root(), + 0, + tree, + headings, + { buf = buf, old = old, fname = fname, to_fname = to_fname, indent = 1 }, + stats + ) + ) + end + + local frontmatter = vim.json.encode({ + title = title, + layout = 'single', -- Hugo-specific, to make _index.html the same as the other pages + aliases = { -- Hugo-specific, make /api.html redirect to /api/ + vim.fs.joinpath('/doc/user', vim.fs.basename(to_fname)), + }, + params = { + firstTag1 = stats.first_tags[1] or '', + firstTag2 = stats.first_tags[2] or '', + basename = vim.fs.basename(fname), + commit = commit, + parseErrors = #stats.parse_errors, + bugUrl = get_bug_url_nvim(fname, to_fname, 'TODO', nil), + noiseLines = html_esc(table.concat(stats.noise_lines, '\n')), + noiseLinesCount = #stats.noise_lines, + headings = headings, + }, + }, { indent = ' ', sort_keys = true }) + + local html = ('%s\n%s'):format(frontmatter, main) + + vim.cmd('q!') + lang_tree:destroy() + return html, stats +end + +--- Generates a JSON map of tags to URL-encoded `filename#anchor` locations. +--- +---@param fname string +local function gen_helptags_json(fname) + assert(tagmap, '`tagmap` not generated yet') + local t = {} ---@type table + for tag, f in pairs(tagmap) do + -- "foo.txt" + local helpfile = vim.fs.basename(f) + -- "foo.html" + local htmlpage = assert(get_helppage_html(helpfile)) + -- "foo.html#tag" + t[tag] = ('%s#%s'):format(htmlpage, url_encode(tag)) + end + tofile(fname, vim.json.encode(t, { indent = ' ', sort_keys = true })) +end + +local function gen_helptag_html(fname) + local frontmatter = vim.json.encode({ + title = 'Helptag redirect', + layout = 'helptag', + aliases = { vim.fs.basename(fname) }, + }, { indent = ' ', sort_keys = true }) + tofile(fname, frontmatter) +end + +-- Testing + +local function ok(cond, expected, actual, message) + assert( + (not expected and not actual) or (expected and actual), + 'if "expected" is given, "actual" is also required' + ) + if expected then + assert( + cond, + ('%sexpected %s, got: %s'):format( + message and (message .. '\n') or '', + vim.inspect(expected), + vim.inspect(actual) + ) + ) + else + assert(cond) + end + + return true +end +local function eq(expected, actual, message) + return ok(vim.deep_equal(expected, actual), expected, actual, message) +end + +function M._test() + tagmap = get_helptags('$VIMRUNTIME/doc') + helpfiles = get_helpfiles(vim.fs.normalize('$VIMRUNTIME/doc')) + + ok(vim.tbl_count(tagmap) > 3000, '>3000', vim.tbl_count(tagmap)) + ok( + vim.endswith(tagmap['vim.diagnostic.set()'], 'diagnostic.txt'), + tagmap['vim.diagnostic.set()'], + 'diagnostic.txt' + ) + ok(vim.endswith(tagmap['%:s'], 'cmdline.txt'), tagmap['%:s'], 'cmdline.txt') + ok(is_noise([[vim:tw=78:isk=!-~,^*,^\|,^\":ts=8:noet:ft=help:norl:]])) + ok(is_noise([[ NVIM REFERENCE MANUAL by Thiago de Arruda ]])) + ok(not is_noise([[vim:tw=78]])) + + eq(0, get_indent('a')) + eq(1, get_indent(' a')) + eq(2, get_indent(' a\n b\n c\n')) + eq(5, get_indent(' a\n \n b\n c\n d\n e\n')) + eq( + 'a\n \n b\n c\n d\n e\n', + trim_indent(' a\n \n b\n c\n d\n e\n') + ) + + local fixed_url, removed_chars = fix_url('https://example.com).') + eq('https://example.com', fixed_url) + eq(').', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com.)') + eq('https://example.com.', fixed_url) + eq(')', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com.') + eq('https://example.com', fixed_url) + eq('.', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com)') + eq('https://example.com', fixed_url) + eq(')', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com') + eq('https://example.com', fixed_url) + eq('', removed_chars) + + print('all tests passed.\n') +end + +--- Generate redirect pages for renamed help files. + +--- @param commit string? +--- @param helpfile string Name of the help file being generated +--- @param to_dir string Target directory where the output files will be written. +--- @param to_fname string Name of the already-generated page in the new location +--- +--- @return boolean Whether a redirect page has been generated +local function gen_redirect_pages(commit, helpfile, to_dir, to_fname) + local helpfile_tag = (helpfile:gsub('%.txt$', '')):gsub('_', '-') -- "dev_tools.txt" => "dev-tools" + local redirect_from = redirects[helpfile] + if not redirect_from then + return false + end + + local redirect_text = vim.text + .indent( + 0, + [[ + *%s* Nvim + + Document moved to: |%s| + + ============================================================================== + Document moved + + Document moved to: |%s| + + ============================================================================== + vim:tw=78:ts=8:ft=help:norl: + ]] + ) + :format(redirect_from, helpfile_tag, helpfile_tag, helpfile_tag, helpfile_tag, helpfile_tag) + local redirect_to_fname = get_helppage_html(redirect_from, true) + local redirect_to = ('%s/%s'):format(to_dir, redirect_to_fname) + local redirect_html, _ = + gen_one_html(redirect_from, redirect_text, redirect_to, false, commit or '?') + assert( + redirect_html:find(vim.pesc(helpfile_tag)), + ('not found in redirect html: %s'):format(helpfile_tag) + ) + tofile(redirect_to, redirect_html) + + print(('generated (redirect) : %-15s => %s'):format(redirect_from, vim.fs.basename(to_fname))) + return true +end + +--- @class nvim.gen_help_html.gen_result +--- @field helpfiles string[] list of generated HTML files, from the source docs {include} +--- @field err_count integer number of parse errors in :help docs +--- @field invalid_links table + +--- Generates HTML from :help docs located in `help_dir` and writes the result in `to_dir`. +--- +--- Example: +--- +--- gen('$VIMRUNTIME/doc', '/path/to/neovim.github.io/_site/doc/', {'api.txt', 'autocmd.txt', 'channel.txt'}, nil) +--- +--- @param output_format string Currently only "html" +--- @param help_dir string Source directory containing the :help files. Must run `make helptags` first. +--- @param to_dir string Target directory where the output files will be written. +--- @param include string[]|nil Process only these filenames. Example: {'api.txt', 'autocmd.txt', 'channel.txt'} +--- @param commit string? +--- @param parser_path string? path to non-default vimdoc.so/dylib/dll +--- +--- @return nvim.gen_help_html.gen_result result +function M.gen(output_format, help_dir, to_dir, include, commit, parser_path) + vim.validate('output_format', output_format, 'string') + vim.validate('help_dir', help_dir, function(d) + return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 + end, 'valid directory') + vim.validate('to_dir', to_dir, 'string') + vim.validate('include', include, 'table', true) + vim.validate('commit', commit, 'string', true) + vim.validate('parser_path', parser_path, function(f) + return vim.fn.filereadable(vim.fs.normalize(f)) == 1 + end, true, 'valid vimdoc.{so,dll,dylib} filepath') + + local err_count = 0 + local redirects_count = 0 + ensure_runtimepath() + + parser_path = parser_path and vim.fs.normalize(parser_path) or nil + if parser_path then + -- XXX: Delete the installed .so files first, else this won't work :( + -- /usr/local/lib/nvim/parser/vimdoc.so + -- ./build/lib/nvim/parser/vimdoc.so + vim.treesitter.language.add('vimdoc', { path = parser_path }) + end + + tagmap = get_helptags(vim.fs.normalize(help_dir)) + helpfiles = get_helpfiles(help_dir, include) + to_dir = vim.fs.normalize(to_dir) + + print(('output dir: %s\n\n'):format(to_dir)) + vim.fn.mkdir(to_dir, 'p') + + if output_format == 'html' then + -- NOTE: Better for Hugo to be in static/, but works fine with contents/ as to_dir + gen_helptags_json(('%s/helptags.json'):format(to_dir)) + gen_helptag_html(('%s/helptag.html'):format(to_dir)) + end + + -- Map output formats to the function that outputs one file in that format + -- NOTE: Only .html for now, but .typ is coming + local generators = { + html = gen_one_html, + } + local gen_one = generators[output_format] + + for _, f in ipairs(helpfiles) do + -- "foo.txt" + local helpfile = vim.fs.basename(f) + + local to_fname = '' + if output_format == 'html' then + -- "to/dir/foo.html" + to_fname = ('%s/%s'):format(to_dir, get_helppage_html(helpfile, true)) + else + to_fname = ('%s/%s.%s'):format(to_dir, helpfile:gsub('.txt', ''), output_format) + end + + local converted_help, stats = gen_one(f, nil, to_fname, not new_layout[helpfile], commit or '?') + tofile(to_fname, converted_help) + print( + ('generated (%-2s errors): %-15s => %s'):format( + #stats.parse_errors, + helpfile, + vim.fs.basename(to_fname) + ) + ) + + if output_format == 'html' then + local generated = gen_redirect_pages(commit, helpfile, to_dir, to_fname) + if generated then + redirects_count = redirects_count + 1 + end + end + + err_count = err_count + #stats.parse_errors + end + + print(('\ngenerated %d html pages'):format(#helpfiles + redirects_count)) + print(('total errors: %d'):format(err_count)) + -- Why aren't the netrw tags found in neovim/docs/ CI? + print(('invalid tags: %s'):format(vim.inspect(invalid_links))) + + if output_format == 'html' then + eq(redirects_count, include and redirects_count or vim.tbl_count(redirects)) -- sanity check + print(('redirects: %d'):format(redirects_count)) + end + + print('\n') + + --- @type nvim.gen_help_html.gen_result + return { + helpfiles = helpfiles, + err_count = err_count, + invalid_links = invalid_links, + } +end + +--- @class nvim.gen_help_html.validate_result +--- @field helpfiles integer number of generated helpfiles +--- @field err_count integer number of parse errors +--- @field parse_errors table +--- @field invalid_links table invalid tags in :help docs +--- @field invalid_urls table invalid URLs in :help docs +--- @field invalid_spelling table> invalid spelling in :help docs + +--- Validates all :help files found in `help_dir`: +--- - checks that |tag| links point to valid helptags. +--- - recursively counts parse errors ("ERROR" nodes) +--- +--- This is 10x faster than gen(), for use in CI. +--- +--- @return nvim.gen_help_html.validate_result result +function M.validate(help_dir, include, parser_path, request_urls) + vim.validate('help_dir', help_dir, function(d) + return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 + end, 'valid directory') + vim.validate('include', include, 'table', true) + vim.validate('parser_path', parser_path, function(f) + return vim.fn.filereadable(vim.fs.normalize(f)) == 1 + end, true, 'valid vimdoc.{so,dll,dylib} filepath') + local err_count = 0 ---@type integer + local files_to_errors = {} ---@type table + ensure_runtimepath() + + parser_path = parser_path and vim.fs.normalize(parser_path) or nil + if parser_path then + -- XXX: Delete the installed .so files first, else this won't work :( + -- /usr/local/lib/nvim/parser/vimdoc.so + -- ./build/lib/nvim/parser/vimdoc.so + vim.treesitter.language.add('vimdoc', { path = parser_path }) + end + + tagmap = get_helptags(vim.fs.normalize(help_dir)) + helpfiles = get_helpfiles(help_dir, include) + + for _, f in ipairs(helpfiles) do + local helpfile = vim.fs.basename(f) + local rv = validate_one(f, request_urls) + print(('validated (%-4s errors): %s'):format(#rv.parse_errors, helpfile)) + if #rv.parse_errors > 0 then + files_to_errors[helpfile] = rv.parse_errors + vim.print(('%s'):format(vim.iter(rv.parse_errors):fold('', function(s, v) + return s .. '\n ' .. v + end))) + end + err_count = err_count + #rv.parse_errors + end + + -- Requests are async, wait for them to finish. + -- TODO(yochem): `:cancel()` tasks after #36146 + vim.wait(20000, function() + return pending_urls <= 0 + end) + ok(pending_urls <= 0, 'pending url checks', pending_urls) + + ---@type nvim.gen_help_html.validate_result + return { + helpfiles = #helpfiles, + err_count = err_count, + parse_errors = files_to_errors, + invalid_links = invalid_links, + invalid_urls = invalid_urls, + invalid_spelling = invalid_spelling, + } +end + +--- Validates vimdoc files in $VIMRUNTIME, and prints error messages on failure. +--- +--- If this fails, try these steps (in order): +--- 1. Fix/cleanup the :help docs. +--- 2. Fix the parser: https://github.com/neovim/tree-sitter-vimdoc +--- 3. File a parser bug, and adjust the tolerance of this test in the meantime. +--- +--- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc' +--- @param request_urls? boolean make network requests to check if the URLs are reachable. +function M.run_validate(help_dir, request_urls) + help_dir = vim.fs.normalize(help_dir or '$VIMRUNTIME/doc') + print('doc path = ' .. vim.uv.fs_realpath(help_dir)) + + local rv = M.validate(help_dir, nil, nil, request_urls) + + -- Check that we actually found helpfiles. + ok(rv.helpfiles > 100, '>100 :help files', rv.helpfiles) + + eq({}, rv.parse_errors, 'no parse errors') + eq(0, rv.err_count, 'no parse errors') + eq({}, rv.invalid_links, 'invalid tags in :help docs') + eq({}, rv.invalid_urls, 'invalid URLs in :help docs') + eq( + {}, + rv.invalid_spelling, + 'invalid spelling in :help docs (see spell_dict in src/gen/gen_help_html.lua)' + ) +end + +--- Test-generates HTML from docs. +--- +--- 1. Test that gen_help_html.lua actually works. +--- 2. Test that parse errors did not increase wildly. Because we explicitly test only a few +--- :help files, we can be precise about the tolerances here. +--- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc' +function M.test_gen(help_dir) + local tmpdir = vim.fs.dirname(vim.fn.tempname()) + help_dir = vim.fs.normalize(help_dir or '$VIMRUNTIME/doc') + print('doc path = ' .. vim.uv.fs_realpath(help_dir)) + + -- Because gen() is slow (~30s), this test is limited to a few files. + local input = { 'api.txt', 'index.txt', 'nvim.txt' } + local rv = M.gen('html', help_dir, tmpdir, input) + eq(#input, #rv.helpfiles) + eq(0, rv.err_count, 'parse errors in :help docs') + eq({}, rv.invalid_links, 'invalid tags in :help docs') +end + +return M diff --git a/tests/testdata/lua_neovim_gen_help_html_change/old.lua b/tests/testdata/lua_neovim_gen_help_html_change/old.lua new file mode 100644 index 0000000..5b82147 --- /dev/null +++ b/tests/testdata/lua_neovim_gen_help_html_change/old.lua @@ -0,0 +1,1374 @@ +--- Converts Nvim :help files to HTML. Validates |tag| links and document syntax (parser errors). +-- +-- USAGE (For CI/local testing purposes): Simply `make lintdoc`, which basically does the following: +-- 1. :helptags ALL +-- 2. nvim -V1 -es +"lua require('src.gen.gen_help_html').run_validate()" +q +-- 3. nvim -V1 -es +"lua require('src.gen.gen_help_html').test_gen()" +q +-- +-- USAGE (GENERATE HTML): +-- 1. `:helptags ALL` first; this script depends on vim.fn.taglist(). +-- 2. nvim -V1 -es --clean +"lua require('src.gen.gen_help_html').gen('html', './runtime/doc', 'target/dir/')" +q +-- - Read the docstring at gen(). +-- 3. cd target/dir/ && jekyll serve --host 0.0.0.0 +-- 4. Visit http://localhost:4000/…/help.txt.html +-- +-- USAGE (VALIDATE): +-- 1. nvim -V1 -es +"lua require('src.gen.gen_help_html').validate('./runtime/doc')" +q +-- - validate() is 10x faster than gen(), so it is used in CI. +-- 2. Check for unreachable URLs: +-- nvim -V1 -es +"lua require('src.gen.gen_help_html').validate('./runtime/doc', true)" +q +-- +-- SELF-TEST MODE: +-- 1. nvim -V1 -es +"lua require('src.gen.gen_help_html')._test()" +q +-- +-- NOTES: +-- * This script is used by the website repo: https://github.com/neovim/neovim.github.io +-- * :helptags checks for duplicate tags, whereas this script checks _links_ (to tags). +-- * gen() and validate() are the primary (programmatic) entrypoints. validate() only exists +-- because gen() is too slow (~1 min) to run in per-commit CI. +-- * ts_node_to_html() is the core function used by gen() to traverse the document tree and produce HTML. +-- * visit_validate() is the core function used by validate(). +-- * Files in `new_layout` will be generated with a "flow" layout instead of preformatted/fixed-width layout. +-- +local pending_urls = 0 +local tagmap = nil ---@type table +local helpfiles = nil ---@type string[] +local invalid_links = {} ---@type table +local invalid_urls = {} ---@type table +local invalid_spelling = {} ---@type table> +local spell_dict = { + Neovim = 'Nvim', + NeoVim = 'Nvim', + neovim = 'Nvim', + lua = 'Lua', + VimL = 'Vimscript', + vimL = 'Vimscript', + viml = 'Vimscript', + ['tree-sitter'] = 'treesitter', + ['Tree-sitter'] = 'Treesitter', +} +--- specify the list of keywords to ignore (i.e. allow), or true to disable spell check completely. +--- @type table +local spell_ignore_files = { + ['credits.txt'] = { 'Neovim' }, + ['news.txt'] = { 'tree-sitter' }, -- in news, may refer to the upstream "tree-sitter" library + ['news-0.10.txt'] = { 'tree-sitter' }, +} +--- Punctuation that indicates a word is part of a path, module name, etc. +--- Example: ".lua" is likely part of a filename, thus we don't want to enforce its spelling. +local spell_punc = { + ['.'] = true, + ['/'] = true, +} +local language = nil + +local M = {} + +-- These files are generated with "flow" layout (non fixed-width, wrapped text paragraphs). +-- All other files are "legacy" files which require fixed-width layout. +local new_layout = { + ['api.txt'] = true, + ['channel.txt'] = true, + ['deprecated.txt'] = true, + ['dev.txt'] = true, + ['dev_arch.txt'] = true, + ['dev_style.txt'] = true, + ['dev_test.txt'] = true, + ['dev_theme.txt'] = true, + ['dev_tools.txt'] = true, + ['dev_vimpatch.txt'] = true, + ['diagnostic.txt'] = true, + ['faq.txt'] = true, + ['gui.txt'] = true, + ['help.txt'] = true, + ['intro.txt'] = true, + ['job_control.txt'] = true, + ['lsp.txt'] = true, + ['lua-bit.txt'] = true, + ['lua-guide.txt'] = true, + ['lua-plugin.txt'] = true, + ['lua.txt'] = true, + ['luaref.txt'] = true, + ['news-0.10.txt'] = true, + ['news-0.11.txt'] = true, + ['news-0.12.txt'] = true, + ['news-0.9.txt'] = true, + ['news.txt'] = true, + ['nvim.txt'] = true, + ['pack.txt'] = true, + ['provider.txt'] = true, + ['starting.txt'] = true, + ['terminal.txt'] = true, + ['tui.txt'] = true, + ['ui.txt'] = true, + ['vim_diff.txt'] = true, +} + +-- Map of new-page:old-page, to redirect renamed pages. +local redirects = { + ['api-ui-events.txt'] = 'ui.txt', + ['credits.txt'] = 'backers.txt', + ['dev.txt'] = 'develop.txt', + ['dev_tools.txt'] = 'debug.txt', + ['plugins.txt'] = 'editorconfig.txt', + ['terminal.txt'] = 'nvim_terminal_emulator.txt', + ['tui.txt'] = 'term.txt', +} + +-- TODO: These known invalid |links| require an update to the relevant docs. +local exclude_invalid = { + matchit = 'vim_diff.txt', +} + +-- False-positive "invalid URLs". +local exclude_invalid_urls = { + ['http://aspell.net/man-html/Affix-Compression.html'] = 'spell.txt', + ['http://aspell.net/man-html/Phonetic-Code.html'] = 'spell.txt', + ['http://lua-users.org/wiki/StringLibraryTutorial'] = 'lua.txt', + ['http://michael.toren.net/code/'] = 'pi_tar.txt', + ['http://oldblog.antirez.com/post/redis-and-scripting.html'] = 'faq.txt', + ['http://papp.plan9.de'] = 'syntax.txt', + ['http://vimcasts.org'] = 'intro.txt', + ['http://wiki.services.openoffice.org/wiki/Dictionaries'] = 'spell.txt', + ['http://www.adapower.com'] = 'ft_ada.txt', + ['http://www.jclark.com/'] = 'quickfix.txt', + + -- Can't be accessed by GitHub runners: + ['https://cacm.acm.org/research/a-look-at-the-design-of-lua/'] = 'faq.txt', + ['https://linux.die.net/man/2/poll'] = 'luvref.txt', + ['https://www.kuwasha.net'] = 'uganda.txt', + ['https://www.kuwasha.net/'] = 'uganda.txt', +} + +-- Deprecated, brain-damaged files that I don't care about. +local ignore_errors = { + ['pi_netrw.txt'] = true, + ['credits.txt'] = true, +} + +local function tofile(fname, text) + local f = io.open(fname, 'w') + if not f then + error(('failed to write: %s'):format(f)) + else + f:write(text) + f:close() + end +end + +---@type fun(s: string): string +local function html_esc(s) + local html_entity = + { ['&'] = '&', ['<'] = '<', ['>'] = '>', ['{'] = '{', ['}'] = '}' } + return (s and string.gsub(s, '[&<>{}]', html_entity) or nil) +end + +local function url_encode(s) + -- Credit: tpope / vim-unimpaired + -- NOTE: these chars intentionally *not* escaped: ' ( ) + return vim.fn.substitute( + vim.fn.iconv(s, 'latin1', 'utf-8'), + [=[[^A-Za-z0-9()'_.~-]]=], + [=[\="%".printf("%02X",char2nr(submatch(0)))]=], + 'g' + ) +end + +local function to_titlecase(s) + local text = '' + for w in vim.gsplit(s, '[ \t]+') do + text = ('%s %s%s'):format(text, vim.fn.toupper(w:sub(1, 1)), w:sub(2)) + end + return text +end + +local function to_heading_tag(text) + -- Prepend "_" to avoid conflicts with actual :help tags. + return text and string.format('_%s', vim.fn.tolower((text:gsub('%s+', '-')))) or 'unknown' +end + +local function basename_noext(f) + return vim.fs.basename(f:gsub('%.txt', '')) +end + +local function is_blank(s) + return not not s:find([[^[\t ]*$]]) +end + +---@type fun(s: string, dir?:0|1|2): string +local function trim(s, dir) + return vim.fn.trim(s, '\r\t\n ', dir or 0) +end + +--- Removes common punctuation from URLs. +--- +--- NOTE: this is currently a no-op, since known issues were fixed in the parser: +--- https://github.com/neovim/tree-sitter-vimdoc/pull/157 +--- +--- @param url string +--- @return string, string (fixed_url, removed_chars) where `removed_chars` is in the order found in the input. +local function fix_url(url) + local removed_chars = '' + local fixed_url = url + -- Remove up to one of each char from end of the URL, in this order. + -- for _, c in ipairs({ '.', ')', ',' }) do + -- if fixed_url:sub(-1) == c then + -- removed_chars = c .. removed_chars + -- fixed_url = fixed_url:sub(1, -2) + -- end + -- end + return fixed_url, removed_chars +end + +--- Checks if a given line is a "noise" line that doesn't look good in HTML form. +local function is_noise(line, noise_lines) + if + -- First line is always noise. + (noise_lines ~= nil and vim.tbl_count(noise_lines) == 0) + or line:find('Type .*gO.* to see the table of contents') + -- Title line of traditional :help pages. + -- Example: "NVIM REFERENCE MANUAL by ..." + or line:find([[^%s*N?VIM[ \t]*REFERENCE[ \t]*MANUAL]]) + -- First line of traditional :help pages. + -- Example: "*api.txt* Nvim" + or line:find('%s*%*?[a-zA-Z]+%.txt%*?%s+N?[vV]im%s*$') + -- modeline + -- Example: "vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:" + or line:find('^%s*vim?%:.*ft=help') + or line:find('^%s*vim?%:.*filetype=help') + or line:find('[*>]local%-additions[*<]') + then + -- table.insert(stats.noise_lines, getbuflinestr(root, opt.buf, 0)) + table.insert(noise_lines or {}, line) + return true + end + return false +end + +--- Creates a github issue URL at neovim/tree-sitter-vimdoc with prefilled content. +--- @param fname string +--- @param to_fname string +--- @param sample_text string +--- @return string +local function get_bug_url_vimdoc(fname, to_fname, sample_text) + local this_url = string.format('https://neovim.io/doc/user/%s', vim.fs.basename(to_fname)) + local bug_url = ( + 'https://github.com/neovim/tree-sitter-vimdoc/issues/new?labels=bug&title=parse+error%3A+' + .. vim.fs.basename(fname) + .. '+&body=Found+%60tree-sitter-vimdoc%60+parse+error+at%3A+' + .. this_url + .. '%0D%0DContext%3A%0D%0D%60%60%60%0D' + .. url_encode(sample_text) + .. '%0D%60%60%60' + ) + return bug_url +end + +--- Creates a github issue URL at neovim/neovim with prefilled content. +--- @param fname string +--- @param to_fname string +--- @param sample_text string +--- @param token_name? string +--- @return string +local function get_bug_url_nvim(fname, to_fname, sample_text, token_name) + local this_url = string.format('https://neovim.io/doc/user/%s', vim.fs.basename(to_fname)) + local bug_url = ( + 'https://github.com/neovim/neovim/issues/new?labels=bug&title=user+docs+HTML%3A+' + .. vim.fs.basename(fname) + .. '+&body=%60gen_help_html.lua%60+problem+at%3A+' + .. this_url + .. '%0D' + .. (token_name and '+unhandled+token%3A+%60' .. token_name .. '%60' or '') + .. '%0DContext%3A%0D%0D%60%60%60%0D' + .. url_encode(sample_text) + .. '%0D%60%60%60' + ) + return bug_url +end + +--- Gets a "foo" name from a "foo.txt" helpfile name. +local function get_helppage_html(f, with_extension) + if not f then + return nil + end + -- Special case: help.txt is the "main landing page" of :help files, not index.txt. + if f == 'index.txt' then + return with_extension and 'vimindex.html' or 'vimindex/' + elseif f == 'help.txt' then + -- Hugo needs an `_index.html` (note the underscore) file to recognize it as section + return with_extension and '_index.html' or '' + end + + return (f:gsub('%.txt$', '')) .. (with_extension and '.html' or '/') +end + +--- Counts leading spaces (tab=8) to decide the indent size of multiline text. +--- +--- Blank lines (empty or whitespace-only) are ignored. +local function get_indent(s) + local _, indent = vim.text.indent(0, s, { expandtab = 8 }) + return indent +end + +--- Removes the common indent level, after expanding tabs to 8 spaces. +local function trim_indent(s) + return (vim.text.indent(0, s, { expandtab = 8 })) +end + +--- Gets raw buffer text in the node's range (+/- an offset), as a newline-delimited string. +---@param node TSNode +---@param bufnr integer +---@param offset integer +local function getbuflinestr(node, bufnr, offset) + local line1, _, line2, _ = node:range() + line1 = line1 - offset + line2 = line2 + offset + local lines = vim.fn.getbufline(bufnr, line1 + 1, line2 + 1) + return table.concat(lines, '\n') +end + +--- Gets the whitespace just before `node` from the raw buffer text. +--- Needed for preformatted `old` lines. +---@param node TSNode +---@param bufnr integer +---@return string +local function getws(node, bufnr) + local line1, c1, line2, _ = node:range() + ---@type string + local raw = vim.fn.getbufline(bufnr, line1 + 1, line2 + 1)[1] + local text_before = raw:sub(1, c1) + local leading_ws = text_before:match('%s+$') or '' + return leading_ws +end + +local function get_tagname(node, bufnr) + local text = vim.treesitter.get_node_text(node, bufnr) + local tag = (node:type() == 'optionlink' or node:parent():type() == 'optionlink') + and ("'%s'"):format(text) + or text + local helpfile = vim.fs.basename(tagmap[tag]) or nil -- "api.txt" + local helppage = get_helppage_html(helpfile) -- "api.html" + return helppage, tag +end + +--- Returns true if the given invalid tagname is a false positive. +local function ignore_invalid(s) + return not not ( + exclude_invalid[s] + -- Strings like |~/====| appear in various places and the parser thinks they are links, but they + -- are just table borders. + or s:find('===') + or s:find('%-%-%-') + ) +end + +local function ignore_parse_error(fname, s) + if ignore_errors[vim.fs.basename(fname)] then + return true + end + -- Ignore parse errors for unclosed tag. + -- This is common in vimdocs and is treated as plaintext by :help. + return s:find("^[`'|*]") +end + +---@param node TSNode +local function has_ancestor(node, ancestor_name) + local p = node ---@type TSNode? + while p do + p = p:parent() + if not p or p:type() == 'help_file' then + break + elseif p:type() == ancestor_name then + return true + end + end + return false +end + +--- Gets the first matching child node matching `name`. +---@param node TSNode +local function first(node, name) + for c, _ in node:iter_children() do + if c:named() and c:type() == name then + return c + end + end + return nil +end + +--- Gets the last child of `node`, or nil. +---@param node TSNode +local function last(node) + local n = node:child_count() + return n > 0 and node:child(n - 1) or nil +end + +--- Gets the kind and node text of the previous and next siblings of node `n`. +--- @param n any node +local function get_prev_next(n) + -- Previous sibling kind (string). + local prev = n:prev_sibling() + and (n:prev_sibling().named and n:prev_sibling():named()) + and n:prev_sibling():type() + or nil + -- Next sibling kind (string). + local next_ = n:next_sibling() + and (n:next_sibling().named and n:next_sibling():named()) + and n:next_sibling():type() + or nil + return prev, next_ +end + +local function validate_link(node, bufnr, fname) + local helppage, tagname = get_tagname(node:child(1), bufnr) + local ignored = false + if not tagmap[tagname] then + ignored = has_ancestor(node, 'column_heading') or node:has_error() or ignore_invalid(tagname) + if not ignored then + invalid_links[tagname] = vim.fs.basename(fname) + end + end + return helppage, tagname, ignored +end + +local function validate_url(text, fname, check_unreachable) + fname = vim.fs.basename(fname) + local ignored = ignore_errors[fname] or exclude_invalid_urls[text] + if ignored then + return true + end + if check_unreachable then + vim.net.request(text, { retry = 2 }, function(err, _) + if err then + invalid_urls[text] = fname + end + pending_urls = pending_urls - 1 + end) + pending_urls = pending_urls + 1 + else + if text:find('http%:') then + invalid_urls[text] = fname + end + end + return false +end + +--- Traverses the tree at `root` and checks that |tag| links point to valid helptags. +---@param root TSNode +---@param level integer +---@param lang_tree TSTree +---@param opt table +---@param stats table +local function visit_validate(root, level, lang_tree, opt, stats) + level = level or 0 + + local function node_text(node) + return vim.treesitter.get_node_text(node or root, opt.buf) + end + + local text = trim(node_text()) + local node_name = (root.named and root:named()) and root:type() or nil + -- Parent kind (string). + local parent = root:parent() and root:parent():type() or nil + local toplevel = level < 1 + -- local prev, next_ = get_prev_next(root) + local prev_text = root:prev_sibling() and node_text(root:prev_sibling()) or nil + local next_text = root:next_sibling() and node_text(root:next_sibling()) or nil + + if root:child_count() > 0 then + for node, _ in root:iter_children() do + if node:named() then + visit_validate(node, level + 1, lang_tree, opt, stats) + end + end + end + + if node_name == 'ERROR' then + if ignore_parse_error(opt.fname, text) then + return + end + -- Store the raw text to give context to the error report. + local sample_text = not toplevel and getbuflinestr(root, opt.buf, 0) or '[top level!]' + -- Flatten the sample text to a single, truncated line. + sample_text = vim.trim(sample_text):gsub('[\t\n]', ' '):sub(1, 80) + table.insert(stats.parse_errors, sample_text) + elseif + (node_name == 'word' or node_name == 'uppercase_name') + and (not vim.tbl_contains({ 'codespan', 'taglink', 'tag' }, parent)) + then + local text_nopunct = vim.fn.trim(text, '.,', 0) -- Ignore some punctuation. + local fname_basename = assert(vim.fs.basename(opt.fname --[[ @as string ]])) + if spell_dict[text_nopunct] then + local should_ignore = ( + spell_ignore_files[fname_basename] == true + or vim.tbl_contains( + (spell_ignore_files[fname_basename] or {}) --[[ @as string[] ]], + text_nopunct + ) + or (spell_punc[next_text] or spell_punc[prev_text]) + ) + if not should_ignore then + invalid_spelling[text_nopunct] = invalid_spelling[text_nopunct] or {} + invalid_spelling[text_nopunct][fname_basename] = node_text(root:parent()) + end + end + elseif node_name == 'url' then + local fixed_url, _ = fix_url(trim(text)) + validate_url(fixed_url, opt.fname, opt.request_urls) + elseif node_name == 'taglink' or node_name == 'optionlink' then + local _, _, _ = validate_link(root, opt.buf, opt.fname) + end +end + +-- Fix tab alignment issues caused by concealed characters like |, `, * in tags +-- and code blocks. +---@param text string +---@param next_node_text string +local function fix_tab_after_conceal(text, next_node_text) + -- Vim tabs take into account the two concealed characters even though they + -- are invisible, so we need to add back in the two spaces if this is + -- followed by a tab to make the tab alignment to match Vim's behavior. + if string.sub(next_node_text, 1, 1) == '\t' then + text = text .. ' ' + end + return text +end + +---@class (exact) nvim.gen_help_html.heading +---@field name string +---@field subheadings nvim.gen_help_html.heading[] +---@field tag string + +-- Generates HTML from node `root` recursively. +---@param root TSNode +---@param level integer +---@param lang_tree TSTree +---@param headings nvim.gen_help_html.heading[] +---@param opt table +---@param stats table +local function ts_node_to_html(root, level, lang_tree, headings, opt, stats) + level = level or 0 + + local function node_text(node, ws_) + node = node or root + ws_ = (ws_ == nil or ws_ == true) and getws(node, opt.buf) or '' + return string.format('%s%s', ws_, vim.treesitter.get_node_text(node, opt.buf)) + end + + -- Gets leading whitespace of `node`. + local function ws(node) + node = node or root + local ws_ = getws(node, opt.buf) + -- XXX: first node of a (line) includes whitespace, even after + -- https://github.com/neovim/tree-sitter-vimdoc/pull/31 ? + if ws_ == '' then + ws_ = vim.treesitter.get_node_text(node, opt.buf):match('^%s+') or '' + end + return ws_ + end + + local node_name = (root.named and root:named()) and root:type() or nil + local prev, next_ = get_prev_next(root) + -- Parent kind (string). + local parent = root:parent() and root:parent():type() or nil + + local text = '' + local trimmed ---@type string + if root:named_child_count() == 0 or node_name == 'ERROR' then + text = node_text() + trimmed = html_esc(trim(text)) + text = html_esc(text) + else + -- Process children and join them with whitespace. + for node, _ in root:iter_children() do + if node:named() then + local r = ts_node_to_html(node, level + 1, lang_tree, headings, opt, stats) + text = string.format('%s%s', text, r) + end + end + trimmed = trim(text) + end + + if node_name == 'help_file' then -- root node + return text + elseif node_name == 'url' then + local fixed_url, removed_chars = fix_url(trimmed) + return ('%s%s%s'):format(ws(), fixed_url, fixed_url, removed_chars) + elseif node_name == 'word' or node_name == 'uppercase_name' then + return text + elseif node_name == 'note' then + return ('%s'):format(text) + elseif node_name == 'h1' or node_name == 'h2' or node_name == 'h3' then + if is_noise(text, stats.noise_lines) then + return '' -- Discard common "noise" lines. + end + -- Remove tags from ToC text. + local heading_node = first(root, 'heading') + local hname = trim(node_text(heading_node):gsub('%*.*%*', '')) + if not heading_node or hname == '' then + return '' -- Spurious "===" or "---" in the help doc. + end + + -- Generate an anchor id from the heading text. + local tagname = to_heading_tag(hname) + if node_name == 'h1' or #headings == 0 then + ---@type nvim.gen_help_html.heading + local heading = { name = hname, subheadings = {}, tag = tagname } + headings[#headings + 1] = heading + else + table.insert( + headings[#headings].subheadings, + { name = hname, subheadings = {}, tag = tagname } + ) + end + local el = node_name == 'h1' and 'h2' or 'h3' + return ('<%s id="%s" class="help-heading">%s\n'):format(el, tagname, trimmed, el) + elseif node_name == 'heading' then + return trimmed + elseif node_name == 'column_heading' or node_name == 'column_name' then + if root:has_error() then + return text + end + return ('
%s
'):format(text) + elseif node_name == 'block' then + if is_blank(text) then + return '' + end + + -- Conjoin list-item blocks: merge adjacent blocks that form a contiguous list. + local prev_block = root:prev_sibling() + local next_block = root:next_sibling() + local prev_last = prev_block and prev_block:type() == 'block' and last(prev_block) + local next_first = next_block and next_block:type() == 'block' and next_block:child(0) + local continues_list = root:child(0) + and root:child(0):type() == 'line_li' + and prev_last + and prev_last:type() == 'line_li' + local list_continues = last(root) + and last(root):type() == 'line_li' + and next_first + and next_first:type() == 'line_li' + if continues_list and list_continues then + return text + elseif continues_list then + -- Last continuation block: close the wrapper opened by the first block. + if opt.old then + return ('%s
\n'):format(trim(text, 2)) + end + return string.format('%s\n
\n', text) + elseif list_continues then + -- First block of a list that continues: open wrapper but don't close. + if opt.old then + return ('
%s'):format(trim(text, 2)) + end + return string.format('
\n%s', text) + end + + if opt.old then + -- XXX: Treat "old" docs as preformatted: they use indentation for layout. + -- Trim trailing newlines to avoid too much whitespace between divs. + return ('
%s
\n'):format(trim(text, 2)) + end + return string.format('
\n%s\n
\n', text) + elseif node_name == 'line' then + if + (parent ~= 'codeblock' or parent ~= 'code') + and (is_blank(text) or is_noise(text, stats.noise_lines)) + then + return '' -- Discard common "noise" lines. + end + -- XXX: Avoid newlines (too much whitespace) after block elements in old (preformatted) layout. + local div = opt.old + and root:child(0) + and vim.list_contains({ 'column_heading', 'h1', 'h2', 'h3' }, root:child(0):type()) + return string.format('%s%s', div and trim(text) or text, div and '' or '\n') + elseif parent == 'line_li' and node_name == 'prefix' then + return '' + elseif node_name == 'line_li' then + local prefix = first(root, 'prefix') + local numli = prefix and trim(node_text(prefix)):match('%d') -- Numbered listitem? + local sib = root:prev_sibling() + local prev_li = sib and sib:type() == 'line_li' + local cssclass = numli and 'help-li-num' or 'help-li' + + -- Conjoin list items separated by blank lines (wrapped in separate blocks). + if not prev_li then + local parent_block = root:parent() + local prev_block = parent_block and parent_block:prev_sibling() + local prev_last = prev_block and prev_block:type() == 'block' and last(prev_block) + if prev_last and prev_last:type() == 'line_li' then + prev_li = true + sib = prev_last + end + end + + if not prev_li then + opt.indent = 1 + else + local sib_ws = ws(sib) + local this_ws = ws() + if get_indent(node_text()) == 0 then + opt.indent = 1 + elseif this_ws > sib_ws then + -- Previous sibling is logically the _parent_ if it is indented less. + opt.indent = opt.indent + 1 + elseif this_ws < sib_ws then + -- TODO(justinmk): This is buggy. Need to track exact whitespace length for each level. + opt.indent = math.max(1, opt.indent - 1) + end + end + local margin = opt.indent == 1 and '' or ('margin-left: %drem;'):format((1.5 * opt.indent)) + + return string.format('
%s
', cssclass, margin, text) + elseif node_name == 'taglink' or node_name == 'optionlink' then + local helppage, tagname, ignored = validate_link(root, opt.buf, opt.fname) + if ignored or not helppage then + return html_esc(node_text(root)) + end + local s = ('%s%s'):format( + ws(), + helppage, + url_encode(tagname), + html_esc(tagname) + ) + if opt.old and node_name == 'taglink' then + s = fix_tab_after_conceal(s, node_text(root:next_sibling())) + end + return s + elseif vim.list_contains({ 'codespan', 'keycode' }, node_name) then + if root:has_error() then + return text + end + local s = ('%s%s'):format(ws(), trimmed) + if opt.old and node_name == 'codespan' then + s = fix_tab_after_conceal(s, node_text(root:next_sibling())) + end + return s + elseif node_name == 'argument' then + return ('%s%s'):format(ws(), trim(node_text(root))) + elseif node_name == 'codeblock' then + return text + elseif node_name == 'language' then + language = node_text(root) + return '' + elseif node_name == 'code' then -- Highlighted codeblock (child). + if is_blank(text) then + return '' + end + local code ---@type string + if language then + code = ('
%s
'):format( + language, + trim(trim_indent(text), 2) + ) + language = nil + else + code = ('
%s
'):format(trim(trim_indent(text), 2)) + end + return code + elseif node_name == 'tag' then -- anchor, h4 pseudo-heading + if root:has_error() then + return text + end + local in_heading = vim.list_contains({ 'h1', 'h2', 'h3' }, parent) + local h4 = not in_heading and not next_ and get_indent(node_text()) > 8 -- h4 pseudo-heading + local cssclass = h4 and 'help-tag-right' or 'help-tag' + local tagname = node_text(root:child(1), false) + if vim.tbl_count(stats.first_tags) < 2 then + -- Force the first 2 tags in the doc to be anchored at the main heading. + table.insert(stats.first_tags, tagname) + return '' + end + local el = 'span' + local encoded_tagname = url_encode(tagname) + local s = ('%s<%s id="%s" class="%s">%s'):format( + ws(), + el, + encoded_tagname, + cssclass, + encoded_tagname, + trimmed, + el + ) + if opt.old then + s = fix_tab_after_conceal(s, node_text(root:next_sibling())) + end + + if in_heading and prev ~= 'tag' then + -- Start the container for tags in a heading. + -- This makes "justify-content:space-between" right-align the tags. + --

foo bartag1 tag2

+ return string.format('%s', s) + elseif in_heading and next_ == nil then + -- End the container for tags in a heading. + return string.format('%s', s) + end + return s .. (h4 and '
' or '') -- HACK:
avoids h4 pseudo-heading mushing with text. + elseif node_name == 'delimiter' or node_name == 'modeline' then + return '' + elseif node_name == 'ERROR' then + if ignore_parse_error(opt.fname, trimmed) then + return text + end + + -- Store the raw text to give context to the bug report. + local sample_text = level > 0 and getbuflinestr(root, opt.buf, 3) or '[top level!]' + table.insert(stats.parse_errors, sample_text) + return ('%s'):format( + get_bug_url_vimdoc(opt.fname, opt.to_fname, sample_text), + trimmed + ) + else -- Unknown token. + local sample_text = level > 0 and getbuflinestr(root, opt.buf, 3) or '[top level!]' + return ('%s'):format( + node_name, + get_bug_url_nvim(opt.fname, opt.to_fname, sample_text, node_name), + trimmed + ), + ('unknown-token:"%s"'):format(node_name) + end +end + +--- @param dir string e.g. '$VIMRUNTIME/doc' +--- @param include string[]|nil +--- @return string[] +local function get_helpfiles(dir, include) + local rv = {} + for f, type in vim.fs.dir(dir) do + if + vim.endswith(f, '.txt') + and type == 'file' + and (not include or vim.list_contains(include, f)) + then + local fullpath = vim.fn.fnamemodify(('%s/%s'):format(dir, f), ':p') + table.insert(rv, fullpath) + end + end + return rv +end + +--- Populates the helptags map. +--- @param help_dir string +--- @return table +local function _get_helptags(help_dir) + ---@type table + local m = {} + -- Load a random help file to convince taglist() to do its job. + vim.cmd(string.format('split %s/api.txt', help_dir)) + vim.cmd('lcd %:p:h') + local tags = vim.fn.taglist('.*') --[[ @as {name: string, filename: string}[] ]] + for _, item in ipairs(tags) do + if vim.endswith(item.filename, '.txt') then + m[item.name] = item.filename + end + end + vim.cmd('q!') + + return m +end + +--- Populates the helptags map. +local function get_helptags(help_dir) + local m = _get_helptags(help_dir) + + --- XXX: Append tags from netrw, until we remove it... + local netrwtags = _get_helptags(vim.fs.normalize('$VIMRUNTIME/pack/dist/opt/netrw/doc/')) + m = vim.tbl_extend('keep', m, netrwtags) + + return m +end + +--- Use the vimdoc parser defined in the build, not whatever happens to be installed on the system. +local function ensure_runtimepath() + if not vim.o.runtimepath:find('build/lib/nvim/') then + vim.cmd [[set runtimepath^=./build/lib/nvim/]] + end +end + +--- Opens `fname` (or `text`, if given) in a buffer and gets a treesitter parser for the buffer contents. +--- +--- @param fname string :help file to parse +--- @param text string? :help file contents +--- @return vim.treesitter.LanguageTree, integer (lang_tree, bufnr) +local function parse_buf(fname, text) + local buf ---@type integer + + if text then + vim.cmd('split new') -- Text contents. + vim.api.nvim_put(vim.split(text, '\n'), '', false, false) + vim.cmd('setfiletype help') + buf = vim.api.nvim_get_current_buf() + elseif type(fname) == 'string' then + vim.cmd('split ' .. vim.fn.fnameescape(fname)) -- Filename. + buf = vim.api.nvim_get_current_buf() + else + -- Left for debugging + ---@diagnostic disable-next-line: no-unknown + buf = fname + vim.cmd('sbuffer ' .. tostring(fname)) -- Buffer number. + end + local lang_tree = assert(vim.treesitter.get_parser(buf, nil)) + lang_tree:parse() + return lang_tree, buf +end + +--- Validates one :help file `fname`: +--- - checks that |tag| links point to valid helptags. +--- - recursively counts parse errors ("ERROR" nodes) +--- +--- @param fname string help file to validate +--- @param request_urls boolean? whether to make requests to the URLs +--- @return { invalid_links: number, parse_errors: string[] } +local function validate_one(fname, request_urls) + local stats = { + parse_errors = {}, + } + local lang_tree, buf = parse_buf(fname, nil) + for _, tree in ipairs(lang_tree:trees()) do + visit_validate(tree:root(), 0, tree, { + buf = buf, + fname = fname, + request_urls = request_urls, + }, stats) + end + lang_tree:destroy() + vim.cmd.close() + return stats +end + +--- Generates HTML from one :help file `fname` and writes the result to `to_fname`. +--- +--- @param fname string Source :help file. +--- @param text string|nil Source :help file contents, or nil to read `fname`. +--- @param to_fname string Destination .html file +--- @param old boolean Preformat paragraphs (for old :help files which are full of arbitrary whitespace) +--- +--- @return string html +--- @return table stats +local function gen_one_html(fname, text, to_fname, old, commit) + local stats = { + noise_lines = {}, + parse_errors = {}, + first_tags = {}, -- Track the first few tags in doc. + } + local lang_tree, buf = parse_buf(fname, text) + ---@type nvim.gen_help_html.heading[] + local headings = {} -- Headings (for ToC). 2-dimensional: h1 contains h2/h3. + local title = to_titlecase(basename_noext(fname)) + + local main = '' + for _, tree in ipairs(lang_tree:trees()) do + main = main + .. ( + ts_node_to_html( + tree:root(), + 0, + tree, + headings, + { buf = buf, old = old, fname = fname, to_fname = to_fname, indent = 1 }, + stats + ) + ) + end + + local frontmatter = vim.json.encode({ + title = title, + layout = 'single', -- Hugo-specific, to make _index.html the same as the other pages + aliases = { -- Hugo-specific, make /api.html redirect to /api/ + vim.fs.joinpath('/doc/user', vim.fs.basename(to_fname)), + }, + params = { + firstTag1 = stats.first_tags[1] or '', + firstTag2 = stats.first_tags[2] or '', + basename = vim.fs.basename(fname), + commit = commit, + parseErrors = #stats.parse_errors, + bugUrl = get_bug_url_nvim(fname, to_fname, 'TODO', nil), + noiseLines = html_esc(table.concat(stats.noise_lines, '\n')), + noiseLinesCount = #stats.noise_lines, + headings = headings, + }, + }, { indent = ' ', sort_keys = true }) + + local html = ('%s\n%s'):format(frontmatter, main) + + vim.cmd('q!') + lang_tree:destroy() + return html, stats +end + +--- Generates a JSON map of tags to URL-encoded `filename#anchor` locations. +--- +---@param fname string +local function gen_helptags_json(fname) + assert(tagmap, '`tagmap` not generated yet') + local t = {} ---@type table + for tag, f in pairs(tagmap) do + -- "foo.txt" + local helpfile = vim.fs.basename(f) + -- "foo.html" + local htmlpage = assert(get_helppage_html(helpfile)) + -- "foo.html#tag" + t[tag] = ('%s#%s'):format(htmlpage, url_encode(tag)) + end + tofile(fname, vim.json.encode(t, { indent = ' ', sort_keys = true })) +end + +local function gen_helptag_html(fname) + local frontmatter = vim.json.encode({ + title = 'Helptag redirect', + layout = 'helptag', + aliases = { vim.fs.basename(fname) }, + }, { indent = ' ', sort_keys = true }) + tofile(fname, frontmatter) +end + +-- Testing + +local function ok(cond, expected, actual, message) + assert( + (not expected and not actual) or (expected and actual), + 'if "expected" is given, "actual" is also required' + ) + if expected then + assert( + cond, + ('%sexpected %s, got: %s'):format( + message and (message .. '\n') or '', + vim.inspect(expected), + vim.inspect(actual) + ) + ) + else + assert(cond) + end + + return true +end +local function eq(expected, actual, message) + return ok(vim.deep_equal(expected, actual), expected, actual, message) +end + +function M._test() + tagmap = get_helptags('$VIMRUNTIME/doc') + helpfiles = get_helpfiles(vim.fs.normalize('$VIMRUNTIME/doc')) + + ok(vim.tbl_count(tagmap) > 3000, '>3000', vim.tbl_count(tagmap)) + ok( + vim.endswith(tagmap['vim.diagnostic.set()'], 'diagnostic.txt'), + tagmap['vim.diagnostic.set()'], + 'diagnostic.txt' + ) + ok(vim.endswith(tagmap['%:s'], 'cmdline.txt'), tagmap['%:s'], 'cmdline.txt') + ok(is_noise([[vim:tw=78:isk=!-~,^*,^\|,^\":ts=8:noet:ft=help:norl:]])) + ok(is_noise([[ NVIM REFERENCE MANUAL by Thiago de Arruda ]])) + ok(not is_noise([[vim:tw=78]])) + + eq(0, get_indent('a')) + eq(1, get_indent(' a')) + eq(2, get_indent(' a\n b\n c\n')) + eq(5, get_indent(' a\n \n b\n c\n d\n e\n')) + eq( + 'a\n \n b\n c\n d\n e\n', + trim_indent(' a\n \n b\n c\n d\n e\n') + ) + + local fixed_url, removed_chars = fix_url('https://example.com).') + eq('https://example.com', fixed_url) + eq(').', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com.)') + eq('https://example.com.', fixed_url) + eq(')', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com.') + eq('https://example.com', fixed_url) + eq('.', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com)') + eq('https://example.com', fixed_url) + eq(')', removed_chars) + fixed_url, removed_chars = fix_url('https://example.com') + eq('https://example.com', fixed_url) + eq('', removed_chars) + + print('all tests passed.\n') +end + +--- Generate redirect pages for renamed help files. + +--- @param commit string? +--- @param helpfile string Name of the help file being generated +--- @param to_dir string Target directory where the output files will be written. +--- @param to_fname string Name of the already-generated page in the new location +--- +--- @return boolean Whether a redirect page has been generated +local function gen_redirect_pages(commit, helpfile, to_dir, to_fname) + local helpfile_tag = (helpfile:gsub('%.txt$', '')):gsub('_', '-') -- "dev_tools.txt" => "dev-tools" + local redirect_from = redirects[helpfile] + if not redirect_from then + return false + end + + local redirect_text = vim.text + .indent( + 0, + [[ + *%s* Nvim + + Document moved to: |%s| + + ============================================================================== + Document moved + + Document moved to: |%s| + + ============================================================================== + vim:tw=78:ts=8:ft=help:norl: + ]] + ) + :format(redirect_from, helpfile_tag, helpfile_tag, helpfile_tag, helpfile_tag, helpfile_tag) + local redirect_to_fname = get_helppage_html(redirect_from, true) + local redirect_to = ('%s/%s'):format(to_dir, redirect_to_fname) + local redirect_html, _ = + gen_one_html(redirect_from, redirect_text, redirect_to, false, commit or '?') + assert( + redirect_html:find(vim.pesc(helpfile_tag)), + ('not found in redirect html: %s'):format(helpfile_tag) + ) + tofile(redirect_to, redirect_html) + + print(('generated (redirect) : %-15s => %s'):format(redirect_from, vim.fs.basename(to_fname))) + return true +end + +--- @class nvim.gen_help_html.gen_result +--- @field helpfiles string[] list of generated HTML files, from the source docs {include} +--- @field err_count integer number of parse errors in :help docs +--- @field invalid_links table + +--- Generates HTML from :help docs located in `help_dir` and writes the result in `to_dir`. +--- +--- Example: +--- +--- gen('$VIMRUNTIME/doc', '/path/to/neovim.github.io/_site/doc/', {'api.txt', 'autocmd.txt', 'channel.txt'}, nil) +--- +--- @param output_format string Currently only "html" +--- @param help_dir string Source directory containing the :help files. Must run `make helptags` first. +--- @param to_dir string Target directory where the output files will be written. +--- @param include string[]|nil Process only these filenames. Example: {'api.txt', 'autocmd.txt', 'channel.txt'} +--- @param commit string? +--- @param parser_path string? path to non-default vimdoc.so/dylib/dll +--- +--- @return nvim.gen_help_html.gen_result result +function M.gen(output_format, help_dir, to_dir, include, commit, parser_path) + vim.validate('output_format', output_format, 'string') + vim.validate('help_dir', help_dir, function(d) + return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 + end, 'valid directory') + vim.validate('to_dir', to_dir, 'string') + vim.validate('include', include, 'table', true) + vim.validate('commit', commit, 'string', true) + vim.validate('parser_path', parser_path, function(f) + return vim.fn.filereadable(vim.fs.normalize(f)) == 1 + end, true, 'valid vimdoc.{so,dll,dylib} filepath') + + local err_count = 0 + local redirects_count = 0 + ensure_runtimepath() + + parser_path = parser_path and vim.fs.normalize(parser_path) or nil + if parser_path then + -- XXX: Delete the installed .so files first, else this won't work :( + -- /usr/local/lib/nvim/parser/vimdoc.so + -- ./build/lib/nvim/parser/vimdoc.so + vim.treesitter.language.add('vimdoc', { path = parser_path }) + end + + tagmap = get_helptags(vim.fs.normalize(help_dir)) + helpfiles = get_helpfiles(help_dir, include) + to_dir = vim.fs.normalize(to_dir) + + print(('output dir: %s\n\n'):format(to_dir)) + vim.fn.mkdir(to_dir, 'p') + + if output_format == 'html' then + -- NOTE: Better for Hugo to be in static/, but works fine with contents/ as to_dir + gen_helptags_json(('%s/helptags.json'):format(to_dir)) + gen_helptag_html(('%s/helptag.html'):format(to_dir)) + end + + -- Map output formats to the function that outputs one file in that format + -- NOTE: Only .html for now, but .typ is coming + local generators = { + html = gen_one_html, + } + local gen_one = generators[output_format] + + for _, f in ipairs(helpfiles) do + -- "foo.txt" + local helpfile = vim.fs.basename(f) + + local to_fname = '' + if output_format == 'html' then + -- "to/dir/foo.html" + to_fname = ('%s/%s'):format(to_dir, get_helppage_html(helpfile, true)) + else + to_fname = ('%s/%s.%s'):format(to_dir, helpfile:gsub('.txt', ''), output_format) + end + + local converted_help, stats = gen_one(f, nil, to_fname, not new_layout[helpfile], commit or '?') + tofile(to_fname, converted_help) + print( + ('generated (%-2s errors): %-15s => %s'):format( + #stats.parse_errors, + helpfile, + vim.fs.basename(to_fname) + ) + ) + + if output_format == 'html' then + local generated = gen_redirect_pages(commit, helpfile, to_dir, to_fname) + if generated then + redirects_count = redirects_count + 1 + end + end + + err_count = err_count + #stats.parse_errors + end + + print(('\ngenerated %d html pages'):format(#helpfiles + redirects_count)) + print(('total errors: %d'):format(err_count)) + -- Why aren't the netrw tags found in neovim/docs/ CI? + print(('invalid tags: %s'):format(vim.inspect(invalid_links))) + + if output_format == 'html' then + eq(redirects_count, include and redirects_count or vim.tbl_count(redirects)) -- sanity check + print(('redirects: %d'):format(redirects_count)) + end + + print('\n') + + --- @type nvim.gen_help_html.gen_result + return { + helpfiles = helpfiles, + err_count = err_count, + invalid_links = invalid_links, + } +end + +--- @class nvim.gen_help_html.validate_result +--- @field helpfiles integer number of generated helpfiles +--- @field err_count integer number of parse errors +--- @field parse_errors table +--- @field invalid_links table invalid tags in :help docs +--- @field invalid_urls table invalid URLs in :help docs +--- @field invalid_spelling table> invalid spelling in :help docs + +--- Validates all :help files found in `help_dir`: +--- - checks that |tag| links point to valid helptags. +--- - recursively counts parse errors ("ERROR" nodes) +--- +--- This is 10x faster than gen(), for use in CI. +--- +--- @return nvim.gen_help_html.validate_result result +function M.validate(help_dir, include, parser_path, request_urls) + vim.validate('help_dir', help_dir, function(d) + return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 + end, 'valid directory') + vim.validate('include', include, 'table', true) + vim.validate('parser_path', parser_path, function(f) + return vim.fn.filereadable(vim.fs.normalize(f)) == 1 + end, true, 'valid vimdoc.{so,dll,dylib} filepath') + local err_count = 0 ---@type integer + local files_to_errors = {} ---@type table + ensure_runtimepath() + + parser_path = parser_path and vim.fs.normalize(parser_path) or nil + if parser_path then + -- XXX: Delete the installed .so files first, else this won't work :( + -- /usr/local/lib/nvim/parser/vimdoc.so + -- ./build/lib/nvim/parser/vimdoc.so + vim.treesitter.language.add('vimdoc', { path = parser_path }) + end + + tagmap = get_helptags(vim.fs.normalize(help_dir)) + helpfiles = get_helpfiles(help_dir, include) + + for _, f in ipairs(helpfiles) do + local helpfile = vim.fs.basename(f) + local rv = validate_one(f, request_urls) + print(('validated (%-4s errors): %s'):format(#rv.parse_errors, helpfile)) + if #rv.parse_errors > 0 then + files_to_errors[helpfile] = rv.parse_errors + vim.print(('%s'):format(vim.iter(rv.parse_errors):fold('', function(s, v) + return s .. '\n ' .. v + end))) + end + err_count = err_count + #rv.parse_errors + end + + -- Requests are async, wait for them to finish. + -- TODO(yochem): `:cancel()` tasks after #36146 + vim.wait(20000, function() + return pending_urls <= 0 + end) + ok(pending_urls <= 0, 'pending url checks', pending_urls) + + ---@type nvim.gen_help_html.validate_result + return { + helpfiles = #helpfiles, + err_count = err_count, + parse_errors = files_to_errors, + invalid_links = invalid_links, + invalid_urls = invalid_urls, + invalid_spelling = invalid_spelling, + } +end + +--- Validates vimdoc files in $VIMRUNTIME, and prints error messages on failure. +--- +--- If this fails, try these steps (in order): +--- 1. Fix/cleanup the :help docs. +--- 2. Fix the parser: https://github.com/neovim/tree-sitter-vimdoc +--- 3. File a parser bug, and adjust the tolerance of this test in the meantime. +--- +--- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc' +--- @param request_urls? boolean make network requests to check if the URLs are reachable. +function M.run_validate(help_dir, request_urls) + help_dir = vim.fs.normalize(help_dir or '$VIMRUNTIME/doc') + print('doc path = ' .. vim.uv.fs_realpath(help_dir)) + + local rv = M.validate(help_dir, nil, nil, request_urls) + + -- Check that we actually found helpfiles. + ok(rv.helpfiles > 100, '>100 :help files', rv.helpfiles) + + eq({}, rv.parse_errors, 'no parse errors') + eq(0, rv.err_count, 'no parse errors') + eq({}, rv.invalid_links, 'invalid tags in :help docs') + eq({}, rv.invalid_urls, 'invalid URLs in :help docs') + eq( + {}, + rv.invalid_spelling, + 'invalid spelling in :help docs (see spell_dict in src/gen/gen_help_html.lua)' + ) +end + +--- Test-generates HTML from docs. +--- +--- 1. Test that gen_help_html.lua actually works. +--- 2. Test that parse errors did not increase wildly. Because we explicitly test only a few +--- :help files, we can be precise about the tolerances here. +--- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc' +function M.test_gen(help_dir) + local tmpdir = vim.fs.dirname(vim.fn.tempname()) + help_dir = vim.fs.normalize(help_dir or '$VIMRUNTIME/doc') + print('doc path = ' .. vim.uv.fs_realpath(help_dir)) + + -- Because gen() is slow (~30s), this test is limited to a few files. + local input = { 'api.txt', 'index.txt', 'nvim.txt' } + local rv = M.gen('html', help_dir, tmpdir, input) + eq(#input, #rv.helpfiles) + eq(0, rv.err_count, 'parse errors in :help docs') + eq({}, rv.invalid_links, 'invalid tags in :help docs') +end + +return M diff --git a/tests/testdata/lua_neovim_ui2_messages/expected.json b/tests/testdata/lua_neovim_ui2_messages/expected.json new file mode 100644 index 0000000..3c64e36 --- /dev/null +++ b/tests/testdata/lua_neovim_ui2_messages/expected.json @@ -0,0 +1,3462 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 76, + 1, + 2, + 0, + 2, + 10, + 3 + ], + "type": "comment", + "start_byte": 26543, + "end_byte": 26619 + }, + "parent": { + "tree": "after", + "path": [ + 76, + 1, + 2, + 0, + 2, + 10 + ], + "type": "if_statement", + "start_byte": 26454, + "end_byte": 26805 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 3 + ], + "type": "comment", + "start_byte": 21712, + "end_byte": 21785 + }, + "parent": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5 + ], + "type": "elseif_statement", + "start_byte": 21679, + "end_byte": 22049 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 4 + ], + "type": "comment", + "start_byte": 21792, + "end_byte": 21872 + }, + "parent": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5 + ], + "type": "elseif_statement", + "start_byte": 21679, + "end_byte": 22049 + }, + "position": 4 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 3 + ], + "type": "comment", + "start_byte": 31099, + "end_byte": 31165 + }, + "parent": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6 + ], + "type": "elseif_statement", + "start_byte": 31066, + "end_byte": 31399 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 4 + ], + "type": "comment", + "start_byte": 31174, + "end_byte": 31254 + }, + "parent": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6 + ], + "type": "elseif_statement", + "start_byte": 31066, + "end_byte": 31399 + }, + "position": 4 + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 4, + 1, + 0, + 0 + ], + "type": "identifier", + "label": "api", + "start_byte": 21871, + "end_byte": 21874 + }, + "old_value": "api", + "new_value": "fn", + "dest_node": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 6, + 0, + 0, + 0 + ], + "type": "identifier", + "label": "fn", + "start_byte": 21963, + "end_byte": 21965 + } + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 4, + 1, + 0, + 1 + ], + "type": "identifier", + "label": "nvim_win_set_cursor", + "start_byte": 21875, + "end_byte": 21894 + }, + "old_value": "nvim_win_set_cursor", + "new_value": "win_execute", + "dest_node": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 6, + 0, + 0, + 1 + ], + "type": "identifier", + "label": "win_execute", + "start_byte": 21966, + "end_byte": 21977 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 6, + 0, + 1, + 1 + ], + "type": "binary_expression", + "start_byte": 21993, + "end_byte": 22048 + }, + "parent": { + "tree": "after", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 6, + 0, + 1 + ], + "type": "arguments", + "start_byte": 21977, + "end_byte": 22049 + }, + "position": 1, + "subtree": true + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 4, + 0, + 0, + 0 + ], + "type": "identifier", + "label": "api", + "start_byte": 30966, + "end_byte": 30969 + }, + "old_value": "api", + "new_value": "fn", + "dest_node": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 6, + 0, + 0, + 0 + ], + "type": "identifier", + "label": "fn", + "start_byte": 31349, + "end_byte": 31351 + } + }, + { + "action": "update", + "node": { + "tree": "before", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 4, + 0, + 0, + 1 + ], + "type": "identifier", + "label": "nvim_win_set_cursor", + "start_byte": 30970, + "end_byte": 30989 + }, + "old_value": "nvim_win_set_cursor", + "new_value": "win_execute", + "dest_node": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 6, + 0, + 0, + 1 + ], + "type": "identifier", + "label": "win_execute", + "start_byte": 31352, + "end_byte": 31363 + } + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 6, + 0, + 1, + 1 + ], + "type": "string", + "label": "'noautocmd norm! Gzb'", + "start_byte": 31377, + "end_byte": 31398 + }, + "parent": { + "tree": "after", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 6, + 0, + 1 + ], + "type": "arguments", + "start_byte": 31363, + "end_byte": 31399 + }, + "position": 1 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 4, + 0 + ], + "type": "variable_declaration", + "start_byte": 21792, + "end_byte": 21864 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 46, + 4, + 4, + 8, + 1, + 7, + 5, + 4, + 1, + 1, + 1 + ], + "type": "table_constructor", + "start_byte": 21910, + "end_byte": 21921 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 84, + 3, + 0, + 3, + 1, + 3, + 9, + 6, + 4, + 0, + 1, + 1 + ], + "type": "table_constructor", + "start_byte": 31003, + "end_byte": 31046 + }, + "subtree": true + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": 376, + "right_line": 376 + }, + { + "left_line": 377, + "right_line": 377 + }, + { + "left_line": 378, + "right_line": 378 + }, + { + "left_line": 379, + "right_line": 379 + }, + { + "left_line": 380, + "right_line": 380 + }, + { + "left_line": 381, + "right_line": 381 + }, + { + "left_line": 382, + "right_line": 382 + }, + { + "left_line": 383, + "right_line": 383 + }, + { + "left_line": 384, + "right_line": 384 + }, + { + "left_line": 385, + "right_line": 385 + }, + { + "left_line": 386, + "right_line": 386 + }, + { + "left_line": 387, + "right_line": 387 + }, + { + "left_line": 388, + "right_line": 388 + }, + { + "left_line": 389, + "right_line": 389 + }, + { + "left_line": 390, + "right_line": 390 + }, + { + "left_line": 391, + "right_line": 391 + }, + { + "left_line": 392, + "right_line": 392 + }, + { + "left_line": 393, + "right_line": 393 + }, + { + "left_line": 394, + "right_line": 394 + }, + { + "left_line": 395, + "right_line": 395 + }, + { + "left_line": 396, + "right_line": 396 + }, + { + "left_line": 397, + "right_line": 397 + }, + { + "left_line": 398, + "right_line": 398 + }, + { + "left_line": 399, + "right_line": 399 + }, + { + "left_line": 400, + "right_line": 400 + }, + { + "left_line": 401, + "right_line": 401 + }, + { + "left_line": 402, + "right_line": 402 + }, + { + "left_line": 403, + "right_line": 403 + }, + { + "left_line": 404, + "right_line": 404 + }, + { + "left_line": 405, + "right_line": 405 + }, + { + "left_line": 406, + "right_line": 406 + }, + { + "left_line": 407, + "right_line": 407 + }, + { + "left_line": 408, + "right_line": 408 + }, + { + "left_line": 409, + "right_line": 409 + }, + { + "left_line": 410, + "right_line": 410 + }, + { + "left_line": 411, + "right_line": 411 + }, + { + "left_line": 412, + "right_line": 412 + }, + { + "left_line": 413, + "right_line": 413 + }, + { + "left_line": 414, + "right_line": 414 + }, + { + "left_line": 415, + "right_line": 415 + }, + { + "left_line": 416, + "right_line": 416 + }, + { + "left_line": 417, + "right_line": 417 + }, + { + "left_line": 418, + "right_line": 418 + }, + { + "left_line": 419, + "right_line": 419 + }, + { + "left_line": 420, + "right_line": 420 + }, + { + "left_line": 421, + "right_line": 421 + }, + { + "left_line": 422, + "right_line": 422 + }, + { + "left_line": 423, + "right_line": 423 + }, + { + "left_line": 424, + "right_line": 424 + }, + { + "left_line": 425, + "right_line": 425 + }, + { + "left_line": 426, + "right_line": 426 + }, + { + "left_line": 427, + "right_line": 427 + }, + { + "left_line": 428, + "right_line": 428 + }, + { + "left_line": 429, + "right_line": 429 + }, + { + "left_line": 430, + "right_line": 430 + }, + { + "left_line": 431, + "right_line": 431 + }, + { + "left_line": 432, + "right_line": 432 + }, + { + "left_line": 433, + "right_line": 433 + }, + { + "left_line": 434, + "right_line": 434 + }, + { + "left_line": 435, + "right_line": 435 + }, + { + "left_line": 436, + "right_line": 436 + }, + { + "left_line": 437, + "right_line": 437 + }, + { + "left_line": 438, + "right_line": 438 + }, + { + "left_line": 439, + "right_line": 439 + }, + { + "left_line": 440, + "right_line": 440 + }, + { + "left_line": 441, + "right_line": 441 + }, + { + "left_line": 442, + "right_line": 442 + }, + { + "left_line": 443, + "right_line": 443 + }, + { + "left_line": 444, + "right_line": 444 + }, + { + "left_line": 445, + "right_line": 445 + }, + { + "left_line": 446, + "right_line": 446 + }, + { + "left_line": 447, + "right_line": 447 + }, + { + "left_line": 448, + "right_line": 448 + }, + { + "left_line": 449, + "right_line": 449 + }, + { + "left_line": 450, + "right_line": 450 + }, + { + "left_line": 451, + "right_line": 451 + }, + { + "left_line": 452, + "right_line": 452 + }, + { + "left_line": 453, + "right_line": 453 + }, + { + "left_line": 454, + "right_line": 454 + }, + { + "left_line": 455, + "right_line": 455 + }, + { + "left_line": 456, + "right_line": 456 + }, + { + "left_line": 457, + "right_line": 457 + }, + { + "left_line": 458, + "right_line": 458 + }, + { + "left_line": 459, + "right_line": 459 + }, + { + "left_line": 460, + "right_line": 460 + }, + { + "left_line": 461, + "right_line": 461 + }, + { + "left_line": 462, + "right_line": 462 + }, + { + "left_line": 463, + "right_line": 463 + }, + { + "left_line": 464, + "right_line": 464 + }, + { + "left_line": 465, + "right_line": 465 + }, + { + "left_line": 466, + "right_line": 466 + }, + { + "left_line": 467, + "right_line": 467 + }, + { + "left_line": 468, + "right_line": 468 + }, + { + "left_line": 469, + "right_line": 469 + }, + { + "left_line": 470, + "right_line": 470 + }, + { + "left_line": 471, + "right_line": 471 + }, + { + "left_line": 472, + "right_line": 472 + }, + { + "left_line": 473, + "right_line": 473 + }, + { + "left_line": 474, + "right_line": 474 + }, + { + "left_line": 475, + "right_line": 475 + }, + { + "left_line": 476, + "right_line": 476 + }, + { + "left_line": 477, + "right_line": 477 + }, + { + "left_line": 478, + "right_line": 478 + }, + { + "left_line": 479, + "right_line": 479 + }, + { + "left_line": 480, + "right_line": 480 + }, + { + "left_line": 481, + "right_line": 481 + }, + { + "left_line": 482, + "right_line": 482 + }, + { + "left_line": 483, + "right_line": 483 + }, + { + "left_line": 484, + "right_line": 484 + }, + { + "left_line": 485, + "right_line": 485 + }, + { + "left_line": 486, + "right_line": 486 + }, + { + "left_line": 487, + "right_line": 487 + }, + { + "left_line": 488, + "right_line": 488 + }, + { + "left_line": -1, + "right_line": 489 + }, + { + "left_line": 489, + "right_line": 490 + }, + { + "left_line": 490, + "right_line": 491 + }, + { + "left_line": 491, + "right_line": 492 + }, + { + "left_line": 492, + "right_line": 493 + }, + { + "left_line": 493, + "right_line": 494 + }, + { + "left_line": 494, + "right_line": 495 + }, + { + "left_line": 495, + "right_line": 496 + }, + { + "left_line": 496, + "right_line": 497 + }, + { + "left_line": 497, + "right_line": 498 + }, + { + "left_line": 498, + "right_line": 499 + }, + { + "left_line": 499, + "right_line": 500 + }, + { + "left_line": 500, + "right_line": 501 + }, + { + "left_line": 501, + "right_line": 502 + }, + { + "left_line": 502, + "right_line": 503 + }, + { + "left_line": 503, + "right_line": 504 + }, + { + "left_line": 504, + "right_line": 505 + }, + { + "left_line": 505, + "right_line": 506 + }, + { + "left_line": 506, + "right_line": 507 + }, + { + "left_line": 507, + "right_line": 508 + }, + { + "left_line": 508, + "right_line": 509 + }, + { + "left_line": 509, + "right_line": 510 + }, + { + "left_line": 510, + "right_line": 511 + }, + { + "left_line": 511, + "right_line": 512 + }, + { + "left_line": 512, + "right_line": 513 + }, + { + "left_line": 513, + "right_line": 514 + }, + { + "left_line": 514, + "right_line": 515 + }, + { + "left_line": 515, + "right_line": 516 + }, + { + "left_line": 516, + "right_line": 517 + }, + { + "left_line": 517, + "right_line": 518 + }, + { + "left_line": 518, + "right_line": 519 + }, + { + "left_line": 519, + "right_line": 520 + }, + { + "left_line": 520, + "right_line": 521 + }, + { + "left_line": 521, + "right_line": 522 + }, + { + "left_line": 522, + "right_line": 523 + }, + { + "left_line": 523, + "right_line": 524 + }, + { + "left_line": 524, + "right_line": 525 + }, + { + "left_line": 525, + "right_line": 526 + }, + { + "left_line": 526, + "right_line": 527 + }, + { + "left_line": 527, + "right_line": 528 + }, + { + "left_line": 528, + "right_line": 529 + }, + { + "left_line": 529, + "right_line": 530 + }, + { + "left_line": 530, + "right_line": 531 + }, + { + "left_line": 531, + "right_line": 532 + }, + { + "left_line": 532, + "right_line": 533 + }, + { + "left_line": 533, + "right_line": 534 + }, + { + "left_line": 534, + "right_line": 535 + }, + { + "left_line": 535, + "right_line": 536 + }, + { + "left_line": 536, + "right_line": 537 + }, + { + "left_line": 537, + "right_line": 538 + }, + { + "left_line": 538, + "right_line": 539 + }, + { + "left_line": 539, + "right_line": 540 + }, + { + "left_line": 540, + "right_line": 541 + }, + { + "left_line": 541, + "right_line": 542 + }, + { + "left_line": 542, + "right_line": 543 + }, + { + "left_line": 543, + "right_line": 544 + }, + { + "left_line": 544, + "right_line": 545 + }, + { + "left_line": 545, + "right_line": 546 + }, + { + "left_line": 546, + "right_line": 547 + }, + { + "left_line": 547, + "right_line": 548 + }, + { + "left_line": 548, + "right_line": 549 + }, + { + "left_line": 549, + "right_line": 550 + }, + { + "left_line": 550, + "right_line": 551 + }, + { + "left_line": 551, + "right_line": 552 + }, + { + "left_line": 552, + "right_line": 553 + }, + { + "left_line": 553, + "right_line": 554 + }, + { + "left_line": 554, + "right_line": 555 + }, + { + "left_line": 555, + "right_line": 556 + }, + { + "left_line": 556, + "right_line": 557 + }, + { + "left_line": 557, + "right_line": 558 + }, + { + "left_line": 558, + "right_line": 559 + }, + { + "left_line": 559, + "right_line": 560 + }, + { + "left_line": 560, + "right_line": 561 + }, + { + "left_line": 561, + "right_line": 562 + }, + { + "left_line": 562, + "right_line": 563 + }, + { + "left_line": 563, + "right_line": 564 + }, + { + "left_line": 564, + "right_line": 565 + }, + { + "left_line": 565, + "right_line": 566 + }, + { + "left_line": 566, + "right_line": 567 + }, + { + "left_line": 567, + "right_line": 568 + }, + { + "left_line": 568, + "right_line": 569 + }, + { + "left_line": 569, + "right_line": 570 + }, + { + "left_line": 570, + "right_line": 571 + }, + { + "left_line": 571, + "right_line": 572 + }, + { + "left_line": 572, + "right_line": 573 + }, + { + "left_line": 573, + "right_line": 574 + }, + { + "left_line": 574, + "right_line": 575 + }, + { + "left_line": 575, + "right_line": 576 + }, + { + "left_line": 576, + "right_line": 577 + }, + { + "left_line": 577, + "right_line": 578 + }, + { + "left_line": 578, + "right_line": 579 + }, + { + "left_line": 579, + "right_line": 580 + }, + { + "left_line": 580, + "right_line": 581 + }, + { + "left_line": 581, + "right_line": 582 + }, + { + "left_line": 582, + "right_line": 583 + }, + { + "left_line": 583, + "right_line": 584 + }, + { + "left_line": 584, + "right_line": 585 + }, + { + "left_line": 585, + "right_line": 586 + }, + { + "left_line": 586, + "right_line": 587 + }, + { + "left_line": 587, + "right_line": 588 + }, + { + "left_line": 588, + "right_line": 589 + }, + { + "left_line": 589, + "right_line": 590 + }, + { + "left_line": 590, + "right_line": 591 + }, + { + "left_line": 591, + "right_line": 592 + }, + { + "left_line": 592, + "right_line": 593 + }, + { + "left_line": 593, + "right_line": 594 + }, + { + "left_line": 594, + "right_line": 595 + }, + { + "left_line": 595, + "right_line": 596 + }, + { + "left_line": 596, + "right_line": 597 + }, + { + "left_line": 597, + "right_line": 598 + }, + { + "left_line": 598, + "right_line": 599 + }, + { + "left_line": 599, + "right_line": 600 + }, + { + "left_line": 600, + "right_line": 601 + }, + { + "left_line": 601, + "right_line": 602 + }, + { + "left_line": 602, + "right_line": 603 + }, + { + "left_line": 603, + "right_line": 604 + }, + { + "left_line": 604, + "right_line": 605 + }, + { + "left_line": 605, + "right_line": 606 + }, + { + "left_line": 606, + "right_line": 607 + }, + { + "left_line": 607, + "right_line": 608 + }, + { + "left_line": 608, + "right_line": 609 + }, + { + "left_line": 609, + "right_line": 610 + }, + { + "left_line": 610, + "right_line": 611 + }, + { + "left_line": 611, + "right_line": 612 + }, + { + "left_line": 612, + "right_line": 613 + }, + { + "left_line": 613, + "right_line": 614 + }, + { + "left_line": 614, + "right_line": 615 + }, + { + "left_line": 615, + "right_line": 616 + }, + { + "left_line": 616, + "right_line": 617 + }, + { + "left_line": 617, + "right_line": 618 + }, + { + "left_line": 618, + "right_line": 619 + }, + { + "left_line": 619, + "right_line": 620 + }, + { + "left_line": 620, + "right_line": 621 + }, + { + "left_line": 621, + "right_line": 622 + }, + { + "left_line": 622, + "right_line": 623 + }, + { + "left_line": 623, + "right_line": 624 + }, + { + "left_line": -1, + "right_line": 625 + }, + { + "left_line": 624, + "right_line": 626 + }, + { + "left_line": 625, + "right_line": 627 + }, + { + "left_line": 626, + "right_line": 628 + }, + { + "left_line": 627, + "right_line": 629 + }, + { + "left_line": 628, + "right_line": 630 + }, + { + "left_line": 629, + "right_line": 631 + }, + { + "left_line": 630, + "right_line": 632 + }, + { + "left_line": 631, + "right_line": 633 + }, + { + "left_line": 632, + "right_line": 634 + }, + { + "left_line": 633, + "right_line": 635 + }, + { + "left_line": 634, + "right_line": 636 + }, + { + "left_line": 635, + "right_line": 637 + }, + { + "left_line": 636, + "right_line": 638 + }, + { + "left_line": 637, + "right_line": 639 + }, + { + "left_line": 638, + "right_line": 640 + }, + { + "left_line": 639, + "right_line": 641 + }, + { + "left_line": 640, + "right_line": 642 + }, + { + "left_line": 641, + "right_line": 643 + }, + { + "left_line": 642, + "right_line": 644 + }, + { + "left_line": 643, + "right_line": 645 + }, + { + "left_line": 644, + "right_line": 646 + }, + { + "left_line": 645, + "right_line": 647 + }, + { + "left_line": 646, + "right_line": 648 + }, + { + "left_line": 647, + "right_line": 649 + }, + { + "left_line": 648, + "right_line": 650 + }, + { + "left_line": 649, + "right_line": 651 + }, + { + "left_line": 650, + "right_line": 652 + }, + { + "left_line": 651, + "right_line": 653 + }, + { + "left_line": 652, + "right_line": 654 + }, + { + "left_line": 653, + "right_line": 655 + }, + { + "left_line": 654, + "right_line": 656 + }, + { + "left_line": 655, + "right_line": 657 + }, + { + "left_line": 656, + "right_line": 658 + }, + { + "left_line": 657, + "right_line": 659 + }, + { + "left_line": 658, + "right_line": 660 + }, + { + "left_line": 659, + "right_line": 661 + }, + { + "left_line": 660, + "right_line": 662 + }, + { + "left_line": 661, + "right_line": 663 + }, + { + "left_line": 662, + "right_line": 664 + }, + { + "left_line": 663, + "right_line": 665 + }, + { + "left_line": 664, + "right_line": 666 + }, + { + "left_line": 665, + "right_line": 667 + }, + { + "left_line": 666, + "right_line": 668 + }, + { + "left_line": 667, + "right_line": 669 + }, + { + "left_line": 668, + "right_line": 670 + }, + { + "left_line": 669, + "right_line": 671 + }, + { + "left_line": 670, + "right_line": 672 + }, + { + "left_line": 671, + "right_line": 673 + }, + { + "left_line": 672, + "right_line": 674 + }, + { + "left_line": 673, + "right_line": 675 + }, + { + "left_line": 674, + "right_line": 676 + }, + { + "left_line": 675, + "right_line": 677 + }, + { + "left_line": 676, + "right_line": 678 + }, + { + "left_line": 677, + "right_line": 679 + }, + { + "left_line": 678, + "right_line": 680 + }, + { + "left_line": 679, + "right_line": 681 + }, + { + "left_line": 680, + "right_line": 682 + }, + { + "left_line": 681, + "right_line": 683 + }, + { + "left_line": 682, + "right_line": 684 + }, + { + "left_line": 683, + "right_line": 685 + }, + { + "left_line": 684, + "right_line": 686 + }, + { + "left_line": 685, + "right_line": 687 + }, + { + "left_line": 686, + "right_line": 688 + }, + { + "left_line": 687, + "right_line": 689 + }, + { + "left_line": 688, + "right_line": 690 + }, + { + "left_line": 689, + "right_line": 691 + }, + { + "left_line": 690, + "right_line": 692 + }, + { + "left_line": 691, + "right_line": 693 + }, + { + "left_line": 692, + "right_line": 694 + }, + { + "left_line": 693, + "right_line": 695 + }, + { + "left_line": 694, + "right_line": 696 + }, + { + "left_line": 695, + "right_line": 697 + }, + { + "left_line": 696, + "right_line": 698 + }, + { + "left_line": 697, + "right_line": 699 + }, + { + "left_line": 698, + "right_line": 700 + }, + { + "left_line": 699, + "right_line": 701 + }, + { + "left_line": 700, + "right_line": 702 + }, + { + "left_line": 701, + "right_line": 703 + }, + { + "left_line": 702, + "right_line": 704 + }, + { + "left_line": 703, + "right_line": 705 + }, + { + "left_line": 704, + "right_line": 706 + }, + { + "left_line": 705, + "right_line": 707 + }, + { + "left_line": 706, + "right_line": 708 + }, + { + "left_line": 707, + "right_line": 709 + }, + { + "left_line": 708, + "right_line": 710 + }, + { + "left_line": 709, + "right_line": 711 + }, + { + "left_line": 710, + "right_line": 712 + }, + { + "left_line": 711, + "right_line": 713 + }, + { + "left_line": 712, + "right_line": 714 + }, + { + "left_line": 713, + "right_line": 715 + }, + { + "left_line": 714, + "right_line": 716 + }, + { + "left_line": 715, + "right_line": 717 + }, + { + "left_line": 716, + "right_line": 718 + }, + { + "left_line": 717, + "right_line": 719 + }, + { + "left_line": 718, + "right_line": 720 + }, + { + "left_line": 719, + "right_line": 721 + }, + { + "left_line": 720, + "right_line": 722 + }, + { + "left_line": -1, + "right_line": 723 + }, + { + "left_line": -1, + "right_line": 724 + }, + { + "left_line": 721, + "right_line": 725 + }, + { + "left_line": 722, + "right_line": 726 + }, + { + "left_line": 723, + "right_line": 727 + }, + { + "left_line": 724, + "right_line": 728 + }, + { + "left_line": 725, + "right_line": 729 + }, + { + "left_line": 726, + "right_line": 730 + }, + { + "left_line": 727, + "right_line": 731 + }, + { + "left_line": 728, + "right_line": 732 + }, + { + "left_line": 729, + "right_line": 733 + }, + { + "left_line": 730, + "right_line": 734 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_ui2_messages/metadata.json b/tests/testdata/lua_neovim_ui2_messages/metadata.json new file mode 100644 index 0000000..f1b786e --- /dev/null +++ b/tests/testdata/lua_neovim_ui2_messages/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "c7707e2a10dc5551f7362f1dedd8c030f920b37d", + "commit": "d343a1e2", + "message": "fix(ui2): position message windows without autocmds (#40867)", + "author": "Barrett Ruth", + "date": "2026-07-20T12:58:30-05:00", + "language": "lua", + "files_changed": 1, + "insertions": 7, + "deletions": 3 +} diff --git a/tests/testdata/lua_neovim_ui2_messages/new.lua b/tests/testdata/lua_neovim_ui2_messages/new.lua new file mode 100644 index 0000000..03d45b4 --- /dev/null +++ b/tests/testdata/lua_neovim_ui2_messages/new.lua @@ -0,0 +1,734 @@ +local api, fn, o = vim.api, vim.fn, vim.o +local nvim_on = require('vim._core.util').nvim_on +local ui = require('vim._core.ui2') + +---@alias Msg { extid: integer, timer: uv.uv_timer_t? } +---@class vim._core.ui2.messages +local M = { + -- Message window. Used for regular messages with cfg.msg.target == 'msg'. + -- Automatically resizes to the text dimensions up to a point, at which point + -- only the most recent messages will fit and be shown. A timer is started for + -- each message whose callback will remove the message from the window again. + msg = { + ids = {}, ---@type table List of visible messages. + width = 1, -- Current width of the message window. + prev_msg = '', -- Concatenated content of the previous message. + prev_id = 0, ---@type string|integer Message id of the previous message. + dupe = 0, -- Number of times message is repeated. + }, + -- Cmdline message window. Used for regular messages with cfg.msg.target == 'cmd'. + -- Also contains 'ruler', 'showcmd' and search_cmd/count messages as virt_text. + -- Messages that don't fit the 'cmdheight' are first shown in an expanded cmdline. + -- Otherwise, or after an expanded cmdline is closed upon the first keypress, the + -- cmdline contains the messages with spilled and duplicate lines indicators. + cmd = { + ids = {}, ---@type table List of visible messages. + msg_row = -1, -- Last row of message to distinguish for placing virt_text. + last_col = o.columns, -- Crop text to start column of 'last' virt_text. + last_emsg = 0, -- Time an error was printed that should not be overwritten. + prev_msg = '', -- Concatenated content of the previous message. + prev_id = 0, ---@type string|integer Message id of the previous message. + dupe = 0, -- Number of times message is repeated. + }, + virt = { -- Stored virt_text state. + last = { {}, {}, {}, {} }, ---@type MsgContent[] status in last cmdline row. + cmd = { {}, {} }, ---@type MsgContent[] [(x)] indicators in cmd window. + msg = { {}, {} }, ---@type MsgContent[] [(x)] indicators in msg window. + top = { {} }, ---@type MsgContent[] [+x] top indicator in dialog window. + bot = { {} }, ---@type MsgContent[] [+x] bottom indicator in dialog window. + idx = { mode = 1, search = 2, cmd = 3, ruler = 4, spill = 1, dupe = 2 }, + ids = {}, ---@type { ['last'|'cmd'|'msg'|'top'|'bot']: integer? } Table of mark IDs. + delayed = false, -- Whether placement of 'last' virt_text is delayed. + }, + dialog_on_key = nil, ---@type integer? vim.on_key namespace for paging in the dialog window. + cmd_on_key = nil, ---@type integer? vim.on_key namespace while cmdline is expanded. +} + +-- An external redraw indicates the start of a new batch of messages in the cmdline. +api.nvim_set_decoration_provider(ui.ns, { + on_start = function() + M.cmd.ids = (ui.redrawing or M.cmd_on_key) and M.cmd.ids or {} + end, +}) + +--- Start a timer whose callback will remove the message from the message window. +--- +---@param buf integer Buffer the message was written to. +---@param id integer|string Message ID. +function M.msg:start_timer(buf, id) + if self.ids[id].timer then + self.ids[id].timer:stop() + end + self.ids[id].timer = vim.defer_fn(function() + -- Postpone message removal when user has entered the msg window. + if api.nvim_get_current_win() == ui.wins.msg then + M.msg:start_timer(buf, id) + return + end + + local extid = api.nvim_buf_is_valid(buf) and self.ids[id] and self.ids[id].extid + local mark = extid and api.nvim_buf_get_extmark_by_id(buf, ui.ns, extid, { details = true }) + self.ids[id] = nil + if not mark or not mark[1] then + return + end + -- Clear prev_msg when line that may have dupe marker is removed. + local erow = api.nvim_buf_line_count(buf) - 1 + self.prev_msg = mark[3].end_row == erow and '' or self.prev_msg + + -- Remove message (including potentially leftover empty line). + api.nvim_buf_set_text(buf, mark[1], mark[2], mark[3].end_row, mark[3].end_col, {}) + if api.nvim_buf_get_lines(buf, mark[1], mark[1] + 1, false)[1] == '' then + api.nvim_buf_set_lines(buf, mark[1], mark[1] + 1, false, {}) + end + + -- Resize or hide message window for removed message. + if next(self.ids) then + M.set_pos('msg') + else + self:clear() + end + end, ui.cfg.msg.msg.timeout) +end + +--- Place or delete a virtual text mark in the cmdline or message window. +--- +---@param type 'last'|'cmd'|'msg'|'top'|'bot' +---@param tgt 'cmd'|'msg'|'dialog'|'pager' +local function set_virttext(type, tgt) + if type == 'last' and (ui.cmdheight == 0 or M.virt.delayed) then + return -- Don't show virtual text while cmdline is expanded or delaying for error. + end + + -- Concatenate the components of M.virt[type] and calculate the concatenated width. + local width, chunks = 0, {} ---@type integer, [string, integer|string][] + local contents = M.virt[type] ---@type MsgContent[] + for _, content in ipairs(contents) do + for _, chunk in ipairs(content) do + chunks[#chunks + 1] = { chunk[2], chunk[3] } + width = width + api.nvim_strwidth(chunk[2]) + end + end + + if M.virt.ids[type] and #chunks == 0 then + api.nvim_buf_del_extmark(ui.bufs[tgt], ui.ns, M.virt.ids[type]) + M.cmd.last_col = type == 'last' and o.columns or M.cmd.last_col + M.virt.ids[type] = nil + elseif #chunks > 0 then + local win = ui.wins[tgt] + local line = (tgt == 'msg' or type == 'top') and 'w0' or type == 'bot' and 'w$' + local srow = line and fn.line(line, ui.wins[tgt]) - 1 + local erow = tgt == 'cmd' and math.min(M.cmd.msg_row, api.nvim_buf_line_count(ui.bufs.cmd) - 1) + local texth = api.nvim_win_text_height(win, { + max_height = (type == 'top' or type == 'bot') and 1 or api.nvim_win_get_height(win), + start_row = srow or nil, + end_row = erow or nil, + }) + local row = texth.end_row + local col = fn.virtcol2col(win, row + 1, texth.end_vcol) + local scol = fn.screenpos(win, row + 1, col).endcol ---@type integer + + if type ~= 'last' then + -- Calculate at which column to place the virt_text such that it is at the end + -- of the last visible message line, overlapping the message text if necessary, + -- but not overlapping the 'last' virt_text. + local offset = tgt ~= 'msg' and 0 + or api.nvim_win_get_position(win)[2] + + (api.nvim_win_get_config(win).border ~= 'none' and 1 or 0) + + -- Check if adding the virt_text on this line will exceed the current window width. + local maxwidth = math.max(M.msg.width, math.min(o.columns, scol - offset + width)) + if tgt == 'msg' and api.nvim_win_get_width(win) < maxwidth then + api.nvim_win_resize(win, maxwidth, -1) + M.msg.width = maxwidth + end + + local mwidth = tgt == 'msg' and M.msg.width or tgt == 'dialog' and o.columns or M.cmd.last_col + if scol - offset + width > mwidth then + col = fn.virtcol2col(win, row + 1, texth.end_vcol - (scol - offset + width - mwidth)) + end + + -- Give virt_text the same highlight as the message tail. + local pos, opts = { row, col }, { details = true, overlap = true, type = 'highlight' } + local hl = api.nvim_buf_get_extmarks(ui.bufs[tgt], ui.ns, pos, pos, opts) + for _, chunk in ipairs(hl[1] and chunks or {}) do + chunk[2] = hl[1][4].hl_group + end + else + local mode = #M.virt.last[M.virt.idx.mode] + local pad = o.columns - width ---@type integer + local newlines = math.max(0, ui.cmdheight - texth.all) + row = row + newlines + M.cmd.last_col = mode > 0 and 0 or o.columns - (newlines > 0 and 0 or width) + + if newlines > 0 then + -- Add empty lines to place virt_text on the last screen row. + api.nvim_buf_set_lines(ui.bufs.cmd, -1, -1, false, fn['repeat']({ '' }, newlines)) + col = 0 + else + if scol > M.cmd.last_col then + -- Give the user some time to read an important message. + if os.time() - M.cmd.last_emsg < 2 then + M.virt.delayed = true + vim.defer_fn(function() + M.virt.delayed = false + set_virttext('last', 'cmd') + end, 2000) + return + end + + -- Crop text on last screen row and find byte offset to place mark at. + local vcol = texth.end_vcol - (scol - M.cmd.last_col) + col = vcol <= 0 and 0 or fn.virtcol2col(win, row + 1, vcol + 1) - 1 + scol = fn.screenpos(win, row + 1, col).endcol + M.cmd.prev_msg = mode > 0 and '' or M.cmd.prev_msg + M.virt.cmd = mode > 0 and { {}, {} } or M.virt.cmd + api.nvim_buf_set_text(ui.bufs.cmd, row, col, row, -1, { mode > 0 and ' ' or '' }) + end + + pad = pad - ((mode > 0 or col == 0) and 0 or math.min(M.cmd.last_col, scol)) + end + table.insert(chunks, mode + 1, { (' '):rep(pad) }) + -- Readjust to new M.cmd.last_col or clear for mode, but don't overwrite + -- locked spill indicator while cmdline is expanded for messages. + if not M.cmd_on_key then + set_virttext('cmd', 'cmd') + end + end + + local opts = { undo_restore = false, invalidate = true, id = M.virt.ids[type] } + opts.priority = (type == 'cmd' or type == 'msg') and 2 or 1 + opts.virt_text_pos = 'overlay' + opts.virt_text = chunks + M.virt.ids[type] = api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, row, col, opts) + end +end + +local hlopts = { undo_restore = false, invalidate = true, priority = 1 } +--- Move messages to expanded cmdline, dialog or pager to show in full. +--- Return updated target+buffer in case it differs from 'src'. +function M.expand_msg(src, tgt) + -- Copy and clear message from src to enlarged cmdline that is dismissed by any + -- key press. Append to pager instead if it isn't hidden or we want to enter it + -- after cmdline was entered during expanded cmdline. + local hidden = api.nvim_win_get_config(ui.wins.pager).hide + tgt = tgt or not hidden and 'pager' or 'cmd' ---@type 'cmd'|'dialog'|'msg'|'pager' + if tgt ~= src then + local srow = hidden and 0 or api.nvim_buf_line_count(ui.bufs.pager) + local opts = { details = true, type = 'highlight' } + local marks = api.nvim_buf_get_extmarks(ui.bufs[src], -1, 0, -1, opts) + local lines = api.nvim_buf_get_lines(ui.bufs[src], 0, -1, false) + -- Clear unless we want to keep the entered command. + if ui.cmd.expand == 0 then + M.msg_clear() + end + if M[src] then + M.virt[src] = { {}, {} } + end + + api.nvim_buf_set_lines(ui.bufs[tgt], srow, -1, false, lines) + for _, m in ipairs(marks) do + hlopts.hl_group, hlopts.end_col, hlopts.end_row = m[4].hl_group, m[4].end_col, m[4].end_row + api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, srow + m[2], m[3], hlopts) + end + else + if M[src] then + M.virt[src][M.virt.idx.dupe][1] = nil + end + for _, id in pairs(M.virt.ids) do + api.nvim_buf_del_extmark(ui.bufs.cmd, ui.ns, id) + end + end + M.set_pos(tgt) + return tgt, ui.bufs[tgt] +end + +local col = 0 -- Current message column appended to with :echon or reset for carriage return. +local cmd_timer ---@type uv.uv_timer_t? Timer resetting cmdline state next event loop. + +---@param tgt 'cmd'|'dialog'|'msg'|'pager' +---@param kind string +---@param content MsgContent +---@param replace_last boolean +---@param append boolean +---@param id integer|string +function M.show_msg(tgt, kind, content, replace_last, append, id) + local mark, msg, cr, dupe, buf = {}, '', false, 0, ui.bufs[tgt] + + if M[tgt] then -- tgt == 'cmd'|'msg' + local extid = M[tgt].ids[id] and M[tgt].ids[id].extid + -- Save the concatenated message to identify repeated messages. + for _, chunk in ipairs(content) do + msg = msg .. chunk[2] + end + local reset = extid or append or msg ~= M[tgt].prev_msg or ui.cmd.srow > 0 + dupe = (reset and 0 or M[tgt].dupe + 1) + + local prev_id = M[tgt].prev_id + cr = next(M[tgt].ids) ~= nil and msg:sub(1, 1) == '\r' + replace_last = next(M[tgt].ids) ~= nil and not extid and (replace_last or dupe > 0) + extid = extid or replace_last and M[tgt].ids[prev_id] and M[tgt].ids[prev_id].extid + mark = extid and api.nvim_buf_get_extmark_by_id(buf, ui.ns, extid, { details = true }) or {} + + -- Ensure cmdline is clear when writing the first message. + if tgt == 'cmd' and dupe == 0 and not next(M.cmd.ids) and ui.cmd.srow == 0 then + api.nvim_buf_set_lines(buf, 0, -1, false, {}) + end + end + + -- Filter out empty newline messages. TODO: don't emit them. + if msg == '\n' then + return + end + + ---@type integer Start row after last line in the target buffer, unless + ---this is the first message, or in case of a repeated or replaced message. + local row = mark[1] + or (M[tgt] and not next(M[tgt].ids) and ui.cmd.srow == 0 and 0) + or (api.nvim_buf_line_count(buf) - ((replace_last or cr or append) and 1 or 0)) + local curline = (cr or append) and api.nvim_buf_get_lines(buf, row, row + 1, false)[1] + col = mark[2] or (append and not cr and math.min(col, #curline) or 0) + local start_row, start_col, lines = row, col, o.lines + + local function set_target_pos() + if tgt == 'msg' then + local width_cmd = [[echo max(map(range(1, line('$')), 'virtcol([v:val, "$"])'))]] + local width = tonumber(fn.win_execute(ui.wins.msg, width_cmd)) - 1 + api.nvim_win_resize(ui.wins.msg, width, -1) + local texth = api.nvim_win_text_height(ui.wins.msg, { start_row = start_row, end_row = row }) + if texth.all > math.ceil(lines * 0.5) then + tgt, buf = M.expand_msg(tgt) + else + M.msg.width = width + M.msg:start_timer(ui.bufs[tgt], id) + M.set_pos(tgt) + end + elseif tgt == 'cmd' and dupe == 0 then + fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights. + if ui.cmd.srow > 0 then + -- In block mode the cmdheight is already dynamic, so just print the full message + -- regardless of height. Put cmdline below message. + ui.cmd.srow = row + 1 + else + -- Place [+x] indicator for lines that spill over 'cmdheight'. + local texth = api.nvim_win_text_height(ui.wins.cmd, { max_height = lines }) + texth.all = math.max(texth.all, api.nvim_buf_line_count(ui.bufs.cmd)) + local spill = texth.all > ui.cmdheight and (' [+%d]'):format(texth.all - ui.cmdheight) + M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil + M.cmd.msg_row = texth.end_row + + -- Expand the cmdline for a non-error message that doesn't fit. + local expand = ui.cfg.msg.cmd.height < 1 or ui.cfg.msg.cmd.height > ui.cmdheight + local error_kinds = { rpc_error = 1, emsg = 1, echoerr = 1, lua_error = 1 } + if expand and texth.all > ui.cmdheight and (ui.cmdheight == 0 or not error_kinds[kind]) then + tgt, buf = M.expand_msg(tgt) + end + end + elseif tgt ~= 'cmd' then + M.set_pos(tgt) + end + end + + local repl, did_cr, code, stime = { '' }, false, nil, vim.uv.hrtime() + for _, chunk in ipairs(content) do + for str in (chunk[2] .. '\0'):gmatch('.-[\n\r%z]') do + local pat, time = str:sub(-1), vim.uv.hrtime() + repl[#repl] = str:sub(1, -2) + repl[#repl + 1] = (pat == '\n' and not did_cr) and '' or nil + + -- Cannot accumulate newlines after a carriage return. + if pat ~= '\n' or did_cr or time - stime > 100000000 then + local end_col = #repl[#repl] + (#repl == 1 and col or 0) + if row >= api.nvim_buf_line_count(buf) then + api.nvim_buf_set_lines(buf, row, -1, false, repl) + else + local erow = mark[3] and mark[3].end_row or row + local ecol = mark[3] and mark[3].end_col or curline and math.min(col + #repl[1], #curline) + api.nvim_buf_set_text(buf, row, col, erow, ecol or -1, repl) + end + + if chunk[3] > 0 then + hlopts.hl_group, hlopts.end_col, hlopts.end_row = chunk[3], end_col, row + #repl - 1 + api.nvim_buf_set_extmark(buf, ui.ns, row, col, hlopts) + end + + row = row + #repl - (did_cr and pat == '\n' and 0 or 1) + col = pat == '\0' and end_col or 0 + curline = #repl > 1 and repl[#repl] or api.nvim_buf_get_lines(buf, row, row + 1, false)[1] + repl, mark[3] = { '' }, nil + did_cr = pat == '\r' + end + + -- Show progress and check for CTRL-C when taking a while. + if time - stime > 100000000 then + stime, _, code = time, vim.wait(0, nil, 0) + if code == -2 then + break -- CTRL-C + end + set_target_pos() + api.nvim__redraw({ flush = true }) + end + end + if code == -2 then + break -- CTRL-C + end + end + + if M[tgt] then + -- Keep track of message span to replace by ID. + local opts = { end_row = row, end_col = col, invalidate = true, undo_restore = false } + M[tgt].ids[id] = M[tgt].ids[id] or {} + M[tgt].ids[id].extid = api.nvim_buf_set_extmark(buf, ui.ns, start_row, start_col, opts) + M[tgt].prev_id, M[tgt].prev_msg, M[tgt].dupe = id, msg, dupe + end + + set_target_pos() + + if M[tgt] and not M.cmd_on_key and row == api.nvim_buf_line_count(buf) - 1 then + -- Place (x) indicator for repeated messages. Mainly to mitigate unnecessary + -- resizing of the message window, but also placed in the cmdline. + M.virt[tgt][M.virt.idx.dupe][1] = dupe > 0 and { 0, ('(%d)'):format(dupe) } or nil + M.virt[tgt][M.virt.idx.spill][1] = tgt == 'cmd' and M.virt.cmd[M.virt.idx.spill][1] or nil + set_virttext(tgt --[[@as 'cmd'|'msg']], tgt) + if tgt == 'cmd' then + -- make sure repeated long messages are cropped to keep the ruler intact + -- (if this is not scheduled, it breaks messages during textlock) + vim.schedule(function() + set_virttext('last', 'cmd') + end) + end + end + + -- Reset message state the next event loop iteration. + if not cmd_timer and (col > 0 or next(M.cmd.ids) ~= nil) then + cmd_timer = vim.defer_fn(function() + M.cmd.ids, cmd_timer, col = M.cmd_on_key and M.cmd.ids or {}, nil, 0 + end, 0) + end +end + +local in_pager = false -- Whether the pager is or will be the current window. +--- Route the message to the appropriate sink. +--- +---@param kind string +---@alias MsgChunk [integer, string, integer] +---@alias MsgContent MsgChunk[] +---@param content MsgContent +---@param replace_last boolean +--@param history boolean +---@param append boolean +---@param id integer|string +---@param trigger string +function M.msg_show(kind, content, replace_last, _, append, id, trigger) + -- Match configured target mappings as Lua pattern to ID: + local k, v, id_target = next(type(id) == 'string' and ui.cfg.msg.targets or {}) + while k and not id_target do + id_target = id:match(k) and v + k, v = next(ui.cfg.msg.targets, k) + end + + -- Set the entered search command in the cmdline (if available). + local tgt = kind == 'search_cmd' and 'cmd' + -- When the pager is open always route typed commands there. This better simulates + -- the UI1 behavior after opening the cmdline below a previous multiline message, + -- and seems useful enough even when the pager was entered manually. + or (trigger == 'typed_cmd' and in_pager and fn.getcmdwintype() == '') and 'pager' + -- Otherwise route to configured target: + or (trigger ~= '' and ui.cfg.msg.targets[trigger]) + or id_target + or (kind ~= '' and ui.cfg.msg.targets[kind]) + or ui.cfg.msg.targets.default + + if kind == 'search_cmd' and ui.cmdheight == 0 then + -- Blocked by messaging() without ext_messages. TODO: look at other messaging() guards. + return + elseif kind == 'empty' then + -- A sole empty message clears the cmdline. + if not next(M.cmd.ids) and ui.cmd.srow == 0 then + M.cmd:clear() + end + elseif kind == 'search_count' then + -- Extract only the search_count, not the entered search command. + -- Match any of search.c:cmdline_search_stat():' [(x | >x | ?)/(y | >y | ??)]' + content = { content[#content] } + content[1][2] = content[1][2]:match('W? %[>?%d*%??/>?%d*%?*%]') .. ' ' + M.virt.last[M.virt.idx.search] = content + M.virt.last[M.virt.idx.cmd] = { { 0, (' '):rep(11) } } + set_virttext('last', 'cmd') + elseif (ui.cmd.prompt or (ui.cmd.level > 0 and tgt == 'cmd')) and ui.cmd.srow == 0 then + -- Route to dialog when a prompt is active, or message would overwrite active cmdline. + replace_last = api.nvim_win_get_config(ui.wins.dialog).hide or kind == 'wildlist' + if kind == 'wildlist' then + api.nvim_buf_set_lines(ui.bufs.dialog, 0, -1, false, {}) + end + ui.cmd.dialog = true -- Ensure dialog is closed when cmdline is hidden. + M.show_msg('dialog', kind, content, replace_last, append, id) + else + if tgt == 'cmd' then + -- Store the time when an important message was emitted in order to not overwrite + -- it with 'last' virt_text in the cmdline so that the user has a chance to read it. + M.cmd.last_emsg = (kind == 'emsg' or kind == 'wmsg') and os.time() or M.cmd.last_emsg + -- Should clear the search count now, mark itself is cleared by invalidate. + M.virt.last[M.virt.idx.search][1] = nil + M.virt.last[M.virt.idx.cmd] = {} + end + -- When message was emitted below an already expanded cmdline, move and route to pager. + tgt = ui.cmd.expand > 0 and 'pager' or tgt + if ui.cmd.expand == 1 then + M.expand_msg('dialog', 'pager') + end + ui.cmd.expand = ui.cmd.expand + (ui.cmd.expand > 0 and 1 or 0) + + local enter_pager = tgt == 'pager' and not in_pager + M.show_msg(tgt, kind, content, replace_last or enter_pager, append, id) + if kind == 'search_cmd' then + -- Don't remember search_cmd message as actual message. + M.cmd.ids, M.cmd.prev_msg = {}, '' + elseif tgt == 'pager' then + -- Position cursor at start of first or last message at bottom of window. + -- Keep Normal commands: nvim_win_set_cursor() only ensures visibility, while zb + -- reliably places the final message at the bottom. :noautocmd avoids #40780. + fn.win_execute(ui.wins.pager, 'noautocmd norm! ' .. (enter_pager and 'gg0' or 'G0zb')) + end + end +end + +--Clear message state in the cmdline. +function M.cmd:clear() + api.nvim_buf_set_lines(ui.bufs.cmd, 0, -1, false, {}) + self.ids, self.prev_msg, self.dupe, self.msg_row = {}, '', 0, -1 + M.virt.cmd, M.virt.last = { {}, {} }, { {}, {}, {}, {} } +end + +--Clear message state in msg window. +function M.msg:clear() + api.nvim_buf_set_lines(ui.bufs.msg, 0, -1, false, {}) + pcall(api.nvim_win_set_config, ui.wins.msg, { hide = true }) + M.virt.msg, self.ids, self.prev_msg, self.dupe, self.width = { {}, {} }, {}, '', 0, 1 +end + +---Clear currently visible messages. +function M.msg_clear() + M.cmd:clear() + M.msg:clear() +end + +--- Place the mode text in the cmdline. +--- +---@param content MsgContent +function M.msg_showmode(content) + M.virt.last[M.virt.idx.mode] = ui.cmd.level > 0 and {} or content + M.virt.last[M.virt.idx.search] = {} + set_virttext('last', 'cmd') +end + +--- Place text from the 'showcmd' buffer in the cmdline. +--- +---@param content MsgContent +function M.msg_showcmd(content) + local str = content[1] and content[1][2]:sub(-10) or '' + M.virt.last[M.virt.idx.cmd][1] = (content[1] or M.virt.last[M.virt.idx.search][1]) + and { 0, str .. (' '):rep(11 - #str) } + set_virttext('last', 'cmd') +end + +--- Place the 'ruler' text in the cmdline window. +--- +---@param content MsgContent +function M.msg_ruler(content) + M.virt.last[M.virt.idx.ruler] = (ui.cmd.level > 0 or M.cmd_on_key) and {} or content + set_virttext('last', 'cmd') +end + +---@alias MsgHistory [string, MsgContent, boolean] +--- Open the message history in the pager. +--- +---@param entries MsgHistory[] +---@param prev_cmd boolean +function M.msg_history_show(entries, prev_cmd) + if #entries == 0 then + return + end + + -- Showing output of previous command, clear in case still visible. + if M.cmd_on_key or prev_cmd then + M.cmd:clear() + end + + api.nvim_buf_set_lines(ui.bufs.pager, 0, -1, false, {}) + for i, entry in ipairs(entries) do + M.show_msg('pager', entry[1], entry[2], i == 1, entry[3], 0) + end + + M.set_pos('pager') +end + +local typed_g = false +local function cmd_on_key(key, typed) + -- Don't dismiss for non-typed keys and mouse movement. When 'g' is passed (typed + -- or mapped), wait until the next key to avoid flickering when the pager is opened. + if not typed_g and (typed == '' or typed == '' or typed == 'g' or key == 'g') then + typed_g = typed == 'g' or key == 'g' + return + end + vim.on_key(nil, ui.ns) + if typed == ':' or ui.cmd.level > 0 then + return -- Keep expanded messages open until cmdline closes. + end + typed = fn.keytrans(typed) + + -- Check if window was entered and reopen with original config. + local mode = not api.nvim_get_mode().mode:match('[it]') + local enter = mode and (typed == '' or typed_g and (typed == '' or key == '<')) + or (typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd) + if enter then + M.expand_msg('cmd', 'pager') + end + pcall(api.nvim_win_close, ui.wins.cmd, true) + ui.check_targets() + api.nvim_win_set_cursor(ui.wins[enter and 'pager' or 'cmd'], { 1, 0 }) + set_virttext('cmd', 'cmd') + api.nvim__redraw({ flush = true }) + + typed_g, M.cmd_on_key, M.cmd.ids = false, nil, {} + return enter and '' or nil +end + +--- Add virtual [+x] text to indicate scrolling is possible. +local function set_top_bot_spill() + local topspill = fn.line('w0', ui.wins.dialog) - 1 + local botspill = api.nvim_buf_line_count(ui.bufs.dialog) - fn.line('w$', ui.wins.dialog) + M.virt.top[1][1] = topspill > 0 and { 0, (' [+%d]'):format(topspill) } or nil + set_virttext('top', 'dialog') + M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil + set_virttext('bot', 'dialog') + api.nvim__redraw({ flush = true }) + return topspill > 0 or botspill > 0 +end + +--- Allow paging in the dialog window, consume the key if the topline changes. +local dialog_on_key = function(_, typed) + typed = typed and fn.keytrans(typed) + if not typed then + return + end + + -- TODO: no hint anymore, so should at least be documented at some point. + local map, eob = {}, typed:match('PageDown') + map[''], map[''] = 'gg', 'G' + map[''], map[''] = 'Hk', 'Hk' + map[''], map[''] = 'Lj', 'Lj' + map[''], map[''] = [[\]], [[\]] + map[''], map[''] = [[\]], [[\]] + + local info = map[typed] and fn.getwininfo(ui.wins.dialog)[1] + if info and (not eob or info.botline < api.nvim_buf_line_count(ui.bufs.dialog)) then + -- Keep Normal commands for screen-relative H/L and page scrolling behavior. + fn.win_execute(ui.wins.dialog, ('exe "norm! %s"'):format(map[typed])) + set_top_bot_spill() + return fn.getwininfo(ui.wins.dialog)[1].topline ~= info.topline and '' or nil + end +end + +local was_cmdwin = '' +---@param min integer Minimum window height. +local function win_row_height_border(tgt, min) + local cfgmin = ui.cfg.msg[tgt].height --[[@as number]] + min = math.min(min, cfgmin < 1 and math.ceil(o.lines * cfgmin) or cfgmin) + if tgt ~= 'pager' then + return (tgt == 'msg' and 0 or 1) - ui.cmd.wmnumode, + math.max(1, min), + min < o.lines - ui.cmdheight + end + local cmdwin = fn.getcmdwintype() ~= was_cmdwin and api.nvim_win_get_height(0) or 0 + local global_stl = (cmdwin > 0 or o.laststatus == 3) and 1 or 0 + local row = 1 - cmdwin - global_stl + local top = min < o.lines - ui.cmdheight - global_stl - cmdwin + local height = math.min(min, o.lines - (top and 1 or 0) - ui.cmdheight - global_stl - cmdwin) + return row, math.max(1, height), top +end + +local function enter_pager() + -- Cannot leave the cmdwin to enter the pager, so close and re-open it. + in_pager, was_cmdwin = true, fn.getcmdwintype() + if was_cmdwin ~= '' then + api.nvim_command('quit') + elseif M.cmd_on_key then + api.nvim_feedkeys(vim.keycode(''), 'tn', false) + end + -- Cmdwin is closed one event iteration later so schedule in case it was open. + vim.schedule(function() + local height, id = api.nvim_win_get_height(ui.wins.pager), 0 + api.nvim_set_option_value('eiw', '', { scope = 'local', win = ui.wins.pager }) + api.nvim_set_current_win(ui.wins.pager) + id = nvim_on({ 'WinEnter', 'CmdwinEnter', 'WinResized' }, ui.augroup, { + desc = 'Hide or reposition pager window.', + }, function(ev) + if fn.getcmdtype() ~= '' then + -- WinEnter fires before we can detect cmdwin will be entered: keep open. + return + elseif ev.event == 'WinResized' and fn.getcmdwintype() == '' then + -- Remember height to be restored when cmdwin is closed. + height = api.nvim_win_get_height(ui.wins.pager) + elseif ev.event == 'WinEnter' then + -- Close when no longer current window. + in_pager = api.nvim_get_current_win() == ui.wins.pager + end + in_pager = in_pager and api.nvim_win_is_valid(ui.wins.pager) + local cfg = in_pager and { relative = 'laststatus', col = 0 } or { hide = true } + if in_pager then + cfg.row, cfg.height, cfg.border = win_row_height_border('pager', height) + else + pcall(api.nvim_set_option_value, 'eiw', 'all', { scope = 'local', win = ui.wins.pager }) + api.nvim_del_autocmd(id) + if was_cmdwin ~= '' then + api.nvim_feedkeys('q' .. was_cmdwin, 'n', false) + was_cmdwin = '' + end + end + pcall(api.nvim_win_set_config, ui.wins.pager, cfg) + end) + end) +end + +--- Adjust visibility and dimensions of the message windows after certain events. +--- +---@param tgt? 'cmd'|'dialog'|'msg'|'pager' Target window to be positioned (nil for all). +function M.set_pos(tgt) + for t, win in pairs(ui.wins) do + local cfg = (t == tgt or (tgt == nil and t ~= 'cmd')) + and api.nvim_win_is_valid(win) + and api.nvim_win_get_config(win) + if cfg and (tgt or not cfg.hide) then + local texth = api.nvim_win_text_height(win, { max_height = o.lines }) + local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' } + cfg = { hide = false, relative = 'laststatus', col = 10000 } ---@type table + cfg.row, cfg.height, cfg.border = win_row_height_border(t, texth.all) + cfg.border = cfg.border and t ~= 'msg' and { '', top, '', '', '', '', '', '' } or nil + cfg.mouse = tgt == 'cmd' or t == 'msg' or nil + api.nvim_win_set_config(win, cfg) + api.nvim_win_set_cursor(win, { 1, 0 }) + + if tgt == 'cmd' then + -- Dismiss temporarily expanded cmdline on next keypress and update spill indicator. + texth.all = math.max(texth.all, api.nvim_buf_line_count(ui.bufs.cmd)) + local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height) + M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil + set_virttext('cmd', 'cmd') + M.virt.cmd[M.virt.idx.spill][1] = { 0, (' [+%d]'):format(texth.all - ui.cmdheight) } + M.cmd_on_key = vim.on_key(cmd_on_key, ui.ns) + elseif tgt == 'dialog' and set_top_bot_spill() then + M.dialog_on_key = vim.on_key(dialog_on_key, M.dialog_on_key) + elseif tgt == 'msg' then + -- Ensure last line is visible and first line is at top of window. + -- Keep Normal commands: nvim_win_set_cursor() only ensures visibility, while zb + -- reliably places the final message at the bottom. :noautocmd avoids #40780. + fn.win_execute(ui.wins.msg, 'noautocmd norm! Gzb') + elseif tgt == 'pager' and not in_pager then + enter_pager() + end + end + end +end + +return M diff --git a/tests/testdata/lua_neovim_ui2_messages/old.lua b/tests/testdata/lua_neovim_ui2_messages/old.lua new file mode 100644 index 0000000..92bca8a --- /dev/null +++ b/tests/testdata/lua_neovim_ui2_messages/old.lua @@ -0,0 +1,730 @@ +local api, fn, o = vim.api, vim.fn, vim.o +local nvim_on = require('vim._core.util').nvim_on +local ui = require('vim._core.ui2') + +---@alias Msg { extid: integer, timer: uv.uv_timer_t? } +---@class vim._core.ui2.messages +local M = { + -- Message window. Used for regular messages with cfg.msg.target == 'msg'. + -- Automatically resizes to the text dimensions up to a point, at which point + -- only the most recent messages will fit and be shown. A timer is started for + -- each message whose callback will remove the message from the window again. + msg = { + ids = {}, ---@type table List of visible messages. + width = 1, -- Current width of the message window. + prev_msg = '', -- Concatenated content of the previous message. + prev_id = 0, ---@type string|integer Message id of the previous message. + dupe = 0, -- Number of times message is repeated. + }, + -- Cmdline message window. Used for regular messages with cfg.msg.target == 'cmd'. + -- Also contains 'ruler', 'showcmd' and search_cmd/count messages as virt_text. + -- Messages that don't fit the 'cmdheight' are first shown in an expanded cmdline. + -- Otherwise, or after an expanded cmdline is closed upon the first keypress, the + -- cmdline contains the messages with spilled and duplicate lines indicators. + cmd = { + ids = {}, ---@type table List of visible messages. + msg_row = -1, -- Last row of message to distinguish for placing virt_text. + last_col = o.columns, -- Crop text to start column of 'last' virt_text. + last_emsg = 0, -- Time an error was printed that should not be overwritten. + prev_msg = '', -- Concatenated content of the previous message. + prev_id = 0, ---@type string|integer Message id of the previous message. + dupe = 0, -- Number of times message is repeated. + }, + virt = { -- Stored virt_text state. + last = { {}, {}, {}, {} }, ---@type MsgContent[] status in last cmdline row. + cmd = { {}, {} }, ---@type MsgContent[] [(x)] indicators in cmd window. + msg = { {}, {} }, ---@type MsgContent[] [(x)] indicators in msg window. + top = { {} }, ---@type MsgContent[] [+x] top indicator in dialog window. + bot = { {} }, ---@type MsgContent[] [+x] bottom indicator in dialog window. + idx = { mode = 1, search = 2, cmd = 3, ruler = 4, spill = 1, dupe = 2 }, + ids = {}, ---@type { ['last'|'cmd'|'msg'|'top'|'bot']: integer? } Table of mark IDs. + delayed = false, -- Whether placement of 'last' virt_text is delayed. + }, + dialog_on_key = nil, ---@type integer? vim.on_key namespace for paging in the dialog window. + cmd_on_key = nil, ---@type integer? vim.on_key namespace while cmdline is expanded. +} + +-- An external redraw indicates the start of a new batch of messages in the cmdline. +api.nvim_set_decoration_provider(ui.ns, { + on_start = function() + M.cmd.ids = (ui.redrawing or M.cmd_on_key) and M.cmd.ids or {} + end, +}) + +--- Start a timer whose callback will remove the message from the message window. +--- +---@param buf integer Buffer the message was written to. +---@param id integer|string Message ID. +function M.msg:start_timer(buf, id) + if self.ids[id].timer then + self.ids[id].timer:stop() + end + self.ids[id].timer = vim.defer_fn(function() + -- Postpone message removal when user has entered the msg window. + if api.nvim_get_current_win() == ui.wins.msg then + M.msg:start_timer(buf, id) + return + end + + local extid = api.nvim_buf_is_valid(buf) and self.ids[id] and self.ids[id].extid + local mark = extid and api.nvim_buf_get_extmark_by_id(buf, ui.ns, extid, { details = true }) + self.ids[id] = nil + if not mark or not mark[1] then + return + end + -- Clear prev_msg when line that may have dupe marker is removed. + local erow = api.nvim_buf_line_count(buf) - 1 + self.prev_msg = mark[3].end_row == erow and '' or self.prev_msg + + -- Remove message (including potentially leftover empty line). + api.nvim_buf_set_text(buf, mark[1], mark[2], mark[3].end_row, mark[3].end_col, {}) + if api.nvim_buf_get_lines(buf, mark[1], mark[1] + 1, false)[1] == '' then + api.nvim_buf_set_lines(buf, mark[1], mark[1] + 1, false, {}) + end + + -- Resize or hide message window for removed message. + if next(self.ids) then + M.set_pos('msg') + else + self:clear() + end + end, ui.cfg.msg.msg.timeout) +end + +--- Place or delete a virtual text mark in the cmdline or message window. +--- +---@param type 'last'|'cmd'|'msg'|'top'|'bot' +---@param tgt 'cmd'|'msg'|'dialog'|'pager' +local function set_virttext(type, tgt) + if type == 'last' and (ui.cmdheight == 0 or M.virt.delayed) then + return -- Don't show virtual text while cmdline is expanded or delaying for error. + end + + -- Concatenate the components of M.virt[type] and calculate the concatenated width. + local width, chunks = 0, {} ---@type integer, [string, integer|string][] + local contents = M.virt[type] ---@type MsgContent[] + for _, content in ipairs(contents) do + for _, chunk in ipairs(content) do + chunks[#chunks + 1] = { chunk[2], chunk[3] } + width = width + api.nvim_strwidth(chunk[2]) + end + end + + if M.virt.ids[type] and #chunks == 0 then + api.nvim_buf_del_extmark(ui.bufs[tgt], ui.ns, M.virt.ids[type]) + M.cmd.last_col = type == 'last' and o.columns or M.cmd.last_col + M.virt.ids[type] = nil + elseif #chunks > 0 then + local win = ui.wins[tgt] + local line = (tgt == 'msg' or type == 'top') and 'w0' or type == 'bot' and 'w$' + local srow = line and fn.line(line, ui.wins[tgt]) - 1 + local erow = tgt == 'cmd' and math.min(M.cmd.msg_row, api.nvim_buf_line_count(ui.bufs.cmd) - 1) + local texth = api.nvim_win_text_height(win, { + max_height = (type == 'top' or type == 'bot') and 1 or api.nvim_win_get_height(win), + start_row = srow or nil, + end_row = erow or nil, + }) + local row = texth.end_row + local col = fn.virtcol2col(win, row + 1, texth.end_vcol) + local scol = fn.screenpos(win, row + 1, col).endcol ---@type integer + + if type ~= 'last' then + -- Calculate at which column to place the virt_text such that it is at the end + -- of the last visible message line, overlapping the message text if necessary, + -- but not overlapping the 'last' virt_text. + local offset = tgt ~= 'msg' and 0 + or api.nvim_win_get_position(win)[2] + + (api.nvim_win_get_config(win).border ~= 'none' and 1 or 0) + + -- Check if adding the virt_text on this line will exceed the current window width. + local maxwidth = math.max(M.msg.width, math.min(o.columns, scol - offset + width)) + if tgt == 'msg' and api.nvim_win_get_width(win) < maxwidth then + api.nvim_win_resize(win, maxwidth, -1) + M.msg.width = maxwidth + end + + local mwidth = tgt == 'msg' and M.msg.width or tgt == 'dialog' and o.columns or M.cmd.last_col + if scol - offset + width > mwidth then + col = fn.virtcol2col(win, row + 1, texth.end_vcol - (scol - offset + width - mwidth)) + end + + -- Give virt_text the same highlight as the message tail. + local pos, opts = { row, col }, { details = true, overlap = true, type = 'highlight' } + local hl = api.nvim_buf_get_extmarks(ui.bufs[tgt], ui.ns, pos, pos, opts) + for _, chunk in ipairs(hl[1] and chunks or {}) do + chunk[2] = hl[1][4].hl_group + end + else + local mode = #M.virt.last[M.virt.idx.mode] + local pad = o.columns - width ---@type integer + local newlines = math.max(0, ui.cmdheight - texth.all) + row = row + newlines + M.cmd.last_col = mode > 0 and 0 or o.columns - (newlines > 0 and 0 or width) + + if newlines > 0 then + -- Add empty lines to place virt_text on the last screen row. + api.nvim_buf_set_lines(ui.bufs.cmd, -1, -1, false, fn['repeat']({ '' }, newlines)) + col = 0 + else + if scol > M.cmd.last_col then + -- Give the user some time to read an important message. + if os.time() - M.cmd.last_emsg < 2 then + M.virt.delayed = true + vim.defer_fn(function() + M.virt.delayed = false + set_virttext('last', 'cmd') + end, 2000) + return + end + + -- Crop text on last screen row and find byte offset to place mark at. + local vcol = texth.end_vcol - (scol - M.cmd.last_col) + col = vcol <= 0 and 0 or fn.virtcol2col(win, row + 1, vcol + 1) - 1 + scol = fn.screenpos(win, row + 1, col).endcol + M.cmd.prev_msg = mode > 0 and '' or M.cmd.prev_msg + M.virt.cmd = mode > 0 and { {}, {} } or M.virt.cmd + api.nvim_buf_set_text(ui.bufs.cmd, row, col, row, -1, { mode > 0 and ' ' or '' }) + end + + pad = pad - ((mode > 0 or col == 0) and 0 or math.min(M.cmd.last_col, scol)) + end + table.insert(chunks, mode + 1, { (' '):rep(pad) }) + -- Readjust to new M.cmd.last_col or clear for mode, but don't overwrite + -- locked spill indicator while cmdline is expanded for messages. + if not M.cmd_on_key then + set_virttext('cmd', 'cmd') + end + end + + local opts = { undo_restore = false, invalidate = true, id = M.virt.ids[type] } + opts.priority = (type == 'cmd' or type == 'msg') and 2 or 1 + opts.virt_text_pos = 'overlay' + opts.virt_text = chunks + M.virt.ids[type] = api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, row, col, opts) + end +end + +local hlopts = { undo_restore = false, invalidate = true, priority = 1 } +--- Move messages to expanded cmdline, dialog or pager to show in full. +--- Return updated target+buffer in case it differs from 'src'. +function M.expand_msg(src, tgt) + -- Copy and clear message from src to enlarged cmdline that is dismissed by any + -- key press. Append to pager instead if it isn't hidden or we want to enter it + -- after cmdline was entered during expanded cmdline. + local hidden = api.nvim_win_get_config(ui.wins.pager).hide + tgt = tgt or not hidden and 'pager' or 'cmd' ---@type 'cmd'|'dialog'|'msg'|'pager' + if tgt ~= src then + local srow = hidden and 0 or api.nvim_buf_line_count(ui.bufs.pager) + local opts = { details = true, type = 'highlight' } + local marks = api.nvim_buf_get_extmarks(ui.bufs[src], -1, 0, -1, opts) + local lines = api.nvim_buf_get_lines(ui.bufs[src], 0, -1, false) + -- Clear unless we want to keep the entered command. + if ui.cmd.expand == 0 then + M.msg_clear() + end + if M[src] then + M.virt[src] = { {}, {} } + end + + api.nvim_buf_set_lines(ui.bufs[tgt], srow, -1, false, lines) + for _, m in ipairs(marks) do + hlopts.hl_group, hlopts.end_col, hlopts.end_row = m[4].hl_group, m[4].end_col, m[4].end_row + api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, srow + m[2], m[3], hlopts) + end + else + if M[src] then + M.virt[src][M.virt.idx.dupe][1] = nil + end + for _, id in pairs(M.virt.ids) do + api.nvim_buf_del_extmark(ui.bufs.cmd, ui.ns, id) + end + end + M.set_pos(tgt) + return tgt, ui.bufs[tgt] +end + +local col = 0 -- Current message column appended to with :echon or reset for carriage return. +local cmd_timer ---@type uv.uv_timer_t? Timer resetting cmdline state next event loop. + +---@param tgt 'cmd'|'dialog'|'msg'|'pager' +---@param kind string +---@param content MsgContent +---@param replace_last boolean +---@param append boolean +---@param id integer|string +function M.show_msg(tgt, kind, content, replace_last, append, id) + local mark, msg, cr, dupe, buf = {}, '', false, 0, ui.bufs[tgt] + + if M[tgt] then -- tgt == 'cmd'|'msg' + local extid = M[tgt].ids[id] and M[tgt].ids[id].extid + -- Save the concatenated message to identify repeated messages. + for _, chunk in ipairs(content) do + msg = msg .. chunk[2] + end + local reset = extid or append or msg ~= M[tgt].prev_msg or ui.cmd.srow > 0 + dupe = (reset and 0 or M[tgt].dupe + 1) + + local prev_id = M[tgt].prev_id + cr = next(M[tgt].ids) ~= nil and msg:sub(1, 1) == '\r' + replace_last = next(M[tgt].ids) ~= nil and not extid and (replace_last or dupe > 0) + extid = extid or replace_last and M[tgt].ids[prev_id] and M[tgt].ids[prev_id].extid + mark = extid and api.nvim_buf_get_extmark_by_id(buf, ui.ns, extid, { details = true }) or {} + + -- Ensure cmdline is clear when writing the first message. + if tgt == 'cmd' and dupe == 0 and not next(M.cmd.ids) and ui.cmd.srow == 0 then + api.nvim_buf_set_lines(buf, 0, -1, false, {}) + end + end + + -- Filter out empty newline messages. TODO: don't emit them. + if msg == '\n' then + return + end + + ---@type integer Start row after last line in the target buffer, unless + ---this is the first message, or in case of a repeated or replaced message. + local row = mark[1] + or (M[tgt] and not next(M[tgt].ids) and ui.cmd.srow == 0 and 0) + or (api.nvim_buf_line_count(buf) - ((replace_last or cr or append) and 1 or 0)) + local curline = (cr or append) and api.nvim_buf_get_lines(buf, row, row + 1, false)[1] + col = mark[2] or (append and not cr and math.min(col, #curline) or 0) + local start_row, start_col, lines = row, col, o.lines + + local function set_target_pos() + if tgt == 'msg' then + local width_cmd = [[echo max(map(range(1, line('$')), 'virtcol([v:val, "$"])'))]] + local width = tonumber(fn.win_execute(ui.wins.msg, width_cmd)) - 1 + api.nvim_win_resize(ui.wins.msg, width, -1) + local texth = api.nvim_win_text_height(ui.wins.msg, { start_row = start_row, end_row = row }) + if texth.all > math.ceil(lines * 0.5) then + tgt, buf = M.expand_msg(tgt) + else + M.msg.width = width + M.msg:start_timer(ui.bufs[tgt], id) + M.set_pos(tgt) + end + elseif tgt == 'cmd' and dupe == 0 then + fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights. + if ui.cmd.srow > 0 then + -- In block mode the cmdheight is already dynamic, so just print the full message + -- regardless of height. Put cmdline below message. + ui.cmd.srow = row + 1 + else + -- Place [+x] indicator for lines that spill over 'cmdheight'. + local texth = api.nvim_win_text_height(ui.wins.cmd, { max_height = lines }) + texth.all = math.max(texth.all, api.nvim_buf_line_count(ui.bufs.cmd)) + local spill = texth.all > ui.cmdheight and (' [+%d]'):format(texth.all - ui.cmdheight) + M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil + M.cmd.msg_row = texth.end_row + + -- Expand the cmdline for a non-error message that doesn't fit. + local expand = ui.cfg.msg.cmd.height < 1 or ui.cfg.msg.cmd.height > ui.cmdheight + local error_kinds = { rpc_error = 1, emsg = 1, echoerr = 1, lua_error = 1 } + if expand and texth.all > ui.cmdheight and (ui.cmdheight == 0 or not error_kinds[kind]) then + tgt, buf = M.expand_msg(tgt) + end + end + elseif tgt ~= 'cmd' then + M.set_pos(tgt) + end + end + + local repl, did_cr, code, stime = { '' }, false, nil, vim.uv.hrtime() + for _, chunk in ipairs(content) do + for str in (chunk[2] .. '\0'):gmatch('.-[\n\r%z]') do + local pat, time = str:sub(-1), vim.uv.hrtime() + repl[#repl] = str:sub(1, -2) + repl[#repl + 1] = (pat == '\n' and not did_cr) and '' or nil + + -- Cannot accumulate newlines after a carriage return. + if pat ~= '\n' or did_cr or time - stime > 100000000 then + local end_col = #repl[#repl] + (#repl == 1 and col or 0) + if row >= api.nvim_buf_line_count(buf) then + api.nvim_buf_set_lines(buf, row, -1, false, repl) + else + local erow = mark[3] and mark[3].end_row or row + local ecol = mark[3] and mark[3].end_col or curline and math.min(col + #repl[1], #curline) + api.nvim_buf_set_text(buf, row, col, erow, ecol or -1, repl) + end + + if chunk[3] > 0 then + hlopts.hl_group, hlopts.end_col, hlopts.end_row = chunk[3], end_col, row + #repl - 1 + api.nvim_buf_set_extmark(buf, ui.ns, row, col, hlopts) + end + + row = row + #repl - (did_cr and pat == '\n' and 0 or 1) + col = pat == '\0' and end_col or 0 + curline = #repl > 1 and repl[#repl] or api.nvim_buf_get_lines(buf, row, row + 1, false)[1] + repl, mark[3] = { '' }, nil + did_cr = pat == '\r' + end + + -- Show progress and check for CTRL-C when taking a while. + if time - stime > 100000000 then + stime, _, code = time, vim.wait(0, nil, 0) + if code == -2 then + break -- CTRL-C + end + set_target_pos() + api.nvim__redraw({ flush = true }) + end + end + if code == -2 then + break -- CTRL-C + end + end + + if M[tgt] then + -- Keep track of message span to replace by ID. + local opts = { end_row = row, end_col = col, invalidate = true, undo_restore = false } + M[tgt].ids[id] = M[tgt].ids[id] or {} + M[tgt].ids[id].extid = api.nvim_buf_set_extmark(buf, ui.ns, start_row, start_col, opts) + M[tgt].prev_id, M[tgt].prev_msg, M[tgt].dupe = id, msg, dupe + end + + set_target_pos() + + if M[tgt] and not M.cmd_on_key and row == api.nvim_buf_line_count(buf) - 1 then + -- Place (x) indicator for repeated messages. Mainly to mitigate unnecessary + -- resizing of the message window, but also placed in the cmdline. + M.virt[tgt][M.virt.idx.dupe][1] = dupe > 0 and { 0, ('(%d)'):format(dupe) } or nil + M.virt[tgt][M.virt.idx.spill][1] = tgt == 'cmd' and M.virt.cmd[M.virt.idx.spill][1] or nil + set_virttext(tgt --[[@as 'cmd'|'msg']], tgt) + if tgt == 'cmd' then + -- make sure repeated long messages are cropped to keep the ruler intact + -- (if this is not scheduled, it breaks messages during textlock) + vim.schedule(function() + set_virttext('last', 'cmd') + end) + end + end + + -- Reset message state the next event loop iteration. + if not cmd_timer and (col > 0 or next(M.cmd.ids) ~= nil) then + cmd_timer = vim.defer_fn(function() + M.cmd.ids, cmd_timer, col = M.cmd_on_key and M.cmd.ids or {}, nil, 0 + end, 0) + end +end + +local in_pager = false -- Whether the pager is or will be the current window. +--- Route the message to the appropriate sink. +--- +---@param kind string +---@alias MsgChunk [integer, string, integer] +---@alias MsgContent MsgChunk[] +---@param content MsgContent +---@param replace_last boolean +--@param history boolean +---@param append boolean +---@param id integer|string +---@param trigger string +function M.msg_show(kind, content, replace_last, _, append, id, trigger) + -- Match configured target mappings as Lua pattern to ID: + local k, v, id_target = next(type(id) == 'string' and ui.cfg.msg.targets or {}) + while k and not id_target do + id_target = id:match(k) and v + k, v = next(ui.cfg.msg.targets, k) + end + + -- Set the entered search command in the cmdline (if available). + local tgt = kind == 'search_cmd' and 'cmd' + -- When the pager is open always route typed commands there. This better simulates + -- the UI1 behavior after opening the cmdline below a previous multiline message, + -- and seems useful enough even when the pager was entered manually. + or (trigger == 'typed_cmd' and in_pager and fn.getcmdwintype() == '') and 'pager' + -- Otherwise route to configured target: + or (trigger ~= '' and ui.cfg.msg.targets[trigger]) + or id_target + or (kind ~= '' and ui.cfg.msg.targets[kind]) + or ui.cfg.msg.targets.default + + if kind == 'search_cmd' and ui.cmdheight == 0 then + -- Blocked by messaging() without ext_messages. TODO: look at other messaging() guards. + return + elseif kind == 'empty' then + -- A sole empty message clears the cmdline. + if not next(M.cmd.ids) and ui.cmd.srow == 0 then + M.cmd:clear() + end + elseif kind == 'search_count' then + -- Extract only the search_count, not the entered search command. + -- Match any of search.c:cmdline_search_stat():' [(x | >x | ?)/(y | >y | ??)]' + content = { content[#content] } + content[1][2] = content[1][2]:match('W? %[>?%d*%??/>?%d*%?*%]') .. ' ' + M.virt.last[M.virt.idx.search] = content + M.virt.last[M.virt.idx.cmd] = { { 0, (' '):rep(11) } } + set_virttext('last', 'cmd') + elseif (ui.cmd.prompt or (ui.cmd.level > 0 and tgt == 'cmd')) and ui.cmd.srow == 0 then + -- Route to dialog when a prompt is active, or message would overwrite active cmdline. + replace_last = api.nvim_win_get_config(ui.wins.dialog).hide or kind == 'wildlist' + if kind == 'wildlist' then + api.nvim_buf_set_lines(ui.bufs.dialog, 0, -1, false, {}) + end + ui.cmd.dialog = true -- Ensure dialog is closed when cmdline is hidden. + M.show_msg('dialog', kind, content, replace_last, append, id) + else + if tgt == 'cmd' then + -- Store the time when an important message was emitted in order to not overwrite + -- it with 'last' virt_text in the cmdline so that the user has a chance to read it. + M.cmd.last_emsg = (kind == 'emsg' or kind == 'wmsg') and os.time() or M.cmd.last_emsg + -- Should clear the search count now, mark itself is cleared by invalidate. + M.virt.last[M.virt.idx.search][1] = nil + M.virt.last[M.virt.idx.cmd] = {} + end + -- When message was emitted below an already expanded cmdline, move and route to pager. + tgt = ui.cmd.expand > 0 and 'pager' or tgt + if ui.cmd.expand == 1 then + M.expand_msg('dialog', 'pager') + end + ui.cmd.expand = ui.cmd.expand + (ui.cmd.expand > 0 and 1 or 0) + + local enter_pager = tgt == 'pager' and not in_pager + M.show_msg(tgt, kind, content, replace_last or enter_pager, append, id) + if kind == 'search_cmd' then + -- Don't remember search_cmd message as actual message. + M.cmd.ids, M.cmd.prev_msg = {}, '' + elseif tgt == 'pager' then + -- Position cursor at start of first or last message at bottom of window. + local lnum = enter_pager and 1 or api.nvim_buf_line_count(ui.bufs.pager) + api.nvim_win_set_cursor(ui.wins.pager, { lnum, 0 }) + end + end +end + +--Clear message state in the cmdline. +function M.cmd:clear() + api.nvim_buf_set_lines(ui.bufs.cmd, 0, -1, false, {}) + self.ids, self.prev_msg, self.dupe, self.msg_row = {}, '', 0, -1 + M.virt.cmd, M.virt.last = { {}, {} }, { {}, {}, {}, {} } +end + +--Clear message state in msg window. +function M.msg:clear() + api.nvim_buf_set_lines(ui.bufs.msg, 0, -1, false, {}) + pcall(api.nvim_win_set_config, ui.wins.msg, { hide = true }) + M.virt.msg, self.ids, self.prev_msg, self.dupe, self.width = { {}, {} }, {}, '', 0, 1 +end + +---Clear currently visible messages. +function M.msg_clear() + M.cmd:clear() + M.msg:clear() +end + +--- Place the mode text in the cmdline. +--- +---@param content MsgContent +function M.msg_showmode(content) + M.virt.last[M.virt.idx.mode] = ui.cmd.level > 0 and {} or content + M.virt.last[M.virt.idx.search] = {} + set_virttext('last', 'cmd') +end + +--- Place text from the 'showcmd' buffer in the cmdline. +--- +---@param content MsgContent +function M.msg_showcmd(content) + local str = content[1] and content[1][2]:sub(-10) or '' + M.virt.last[M.virt.idx.cmd][1] = (content[1] or M.virt.last[M.virt.idx.search][1]) + and { 0, str .. (' '):rep(11 - #str) } + set_virttext('last', 'cmd') +end + +--- Place the 'ruler' text in the cmdline window. +--- +---@param content MsgContent +function M.msg_ruler(content) + M.virt.last[M.virt.idx.ruler] = (ui.cmd.level > 0 or M.cmd_on_key) and {} or content + set_virttext('last', 'cmd') +end + +---@alias MsgHistory [string, MsgContent, boolean] +--- Open the message history in the pager. +--- +---@param entries MsgHistory[] +---@param prev_cmd boolean +function M.msg_history_show(entries, prev_cmd) + if #entries == 0 then + return + end + + -- Showing output of previous command, clear in case still visible. + if M.cmd_on_key or prev_cmd then + M.cmd:clear() + end + + api.nvim_buf_set_lines(ui.bufs.pager, 0, -1, false, {}) + for i, entry in ipairs(entries) do + M.show_msg('pager', entry[1], entry[2], i == 1, entry[3], 0) + end + + M.set_pos('pager') +end + +local typed_g = false +local function cmd_on_key(key, typed) + -- Don't dismiss for non-typed keys and mouse movement. When 'g' is passed (typed + -- or mapped), wait until the next key to avoid flickering when the pager is opened. + if not typed_g and (typed == '' or typed == '' or typed == 'g' or key == 'g') then + typed_g = typed == 'g' or key == 'g' + return + end + vim.on_key(nil, ui.ns) + if typed == ':' or ui.cmd.level > 0 then + return -- Keep expanded messages open until cmdline closes. + end + typed = fn.keytrans(typed) + + -- Check if window was entered and reopen with original config. + local mode = not api.nvim_get_mode().mode:match('[it]') + local enter = mode and (typed == '' or typed_g and (typed == '' or key == '<')) + or (typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd) + if enter then + M.expand_msg('cmd', 'pager') + end + pcall(api.nvim_win_close, ui.wins.cmd, true) + ui.check_targets() + api.nvim_win_set_cursor(ui.wins[enter and 'pager' or 'cmd'], { 1, 0 }) + set_virttext('cmd', 'cmd') + api.nvim__redraw({ flush = true }) + + typed_g, M.cmd_on_key, M.cmd.ids = false, nil, {} + return enter and '' or nil +end + +--- Add virtual [+x] text to indicate scrolling is possible. +local function set_top_bot_spill() + local topspill = fn.line('w0', ui.wins.dialog) - 1 + local botspill = api.nvim_buf_line_count(ui.bufs.dialog) - fn.line('w$', ui.wins.dialog) + M.virt.top[1][1] = topspill > 0 and { 0, (' [+%d]'):format(topspill) } or nil + set_virttext('top', 'dialog') + M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil + set_virttext('bot', 'dialog') + api.nvim__redraw({ flush = true }) + return topspill > 0 or botspill > 0 +end + +--- Allow paging in the dialog window, consume the key if the topline changes. +local dialog_on_key = function(_, typed) + typed = typed and fn.keytrans(typed) + if not typed then + return + end + + -- TODO: no hint anymore, so should at least be documented at some point. + local map, eob = {}, typed:match('PageDown') + map[''], map[''] = 'gg', 'G' + map[''], map[''] = 'Hk', 'Hk' + map[''], map[''] = 'Lj', 'Lj' + map[''], map[''] = [[\]], [[\]] + map[''], map[''] = [[\]], [[\]] + + local info = map[typed] and fn.getwininfo(ui.wins.dialog)[1] + if info and (not eob or info.botline < api.nvim_buf_line_count(ui.bufs.dialog)) then + fn.win_execute(ui.wins.dialog, ('exe "norm! %s"'):format(map[typed])) + set_top_bot_spill() + return fn.getwininfo(ui.wins.dialog)[1].topline ~= info.topline and '' or nil + end +end + +local was_cmdwin = '' +---@param min integer Minimum window height. +local function win_row_height_border(tgt, min) + local cfgmin = ui.cfg.msg[tgt].height --[[@as number]] + min = math.min(min, cfgmin < 1 and math.ceil(o.lines * cfgmin) or cfgmin) + if tgt ~= 'pager' then + return (tgt == 'msg' and 0 or 1) - ui.cmd.wmnumode, + math.max(1, min), + min < o.lines - ui.cmdheight + end + local cmdwin = fn.getcmdwintype() ~= was_cmdwin and api.nvim_win_get_height(0) or 0 + local global_stl = (cmdwin > 0 or o.laststatus == 3) and 1 or 0 + local row = 1 - cmdwin - global_stl + local top = min < o.lines - ui.cmdheight - global_stl - cmdwin + local height = math.min(min, o.lines - (top and 1 or 0) - ui.cmdheight - global_stl - cmdwin) + return row, math.max(1, height), top +end + +local function enter_pager() + -- Cannot leave the cmdwin to enter the pager, so close and re-open it. + in_pager, was_cmdwin = true, fn.getcmdwintype() + if was_cmdwin ~= '' then + api.nvim_command('quit') + elseif M.cmd_on_key then + api.nvim_feedkeys(vim.keycode(''), 'tn', false) + end + -- Cmdwin is closed one event iteration later so schedule in case it was open. + vim.schedule(function() + local height, id = api.nvim_win_get_height(ui.wins.pager), 0 + api.nvim_set_option_value('eiw', '', { scope = 'local', win = ui.wins.pager }) + api.nvim_set_current_win(ui.wins.pager) + id = nvim_on({ 'WinEnter', 'CmdwinEnter', 'WinResized' }, ui.augroup, { + desc = 'Hide or reposition pager window.', + }, function(ev) + if fn.getcmdtype() ~= '' then + -- WinEnter fires before we can detect cmdwin will be entered: keep open. + return + elseif ev.event == 'WinResized' and fn.getcmdwintype() == '' then + -- Remember height to be restored when cmdwin is closed. + height = api.nvim_win_get_height(ui.wins.pager) + elseif ev.event == 'WinEnter' then + -- Close when no longer current window. + in_pager = api.nvim_get_current_win() == ui.wins.pager + end + in_pager = in_pager and api.nvim_win_is_valid(ui.wins.pager) + local cfg = in_pager and { relative = 'laststatus', col = 0 } or { hide = true } + if in_pager then + cfg.row, cfg.height, cfg.border = win_row_height_border('pager', height) + else + pcall(api.nvim_set_option_value, 'eiw', 'all', { scope = 'local', win = ui.wins.pager }) + api.nvim_del_autocmd(id) + if was_cmdwin ~= '' then + api.nvim_feedkeys('q' .. was_cmdwin, 'n', false) + was_cmdwin = '' + end + end + pcall(api.nvim_win_set_config, ui.wins.pager, cfg) + end) + end) +end + +--- Adjust visibility and dimensions of the message windows after certain events. +--- +---@param tgt? 'cmd'|'dialog'|'msg'|'pager' Target window to be positioned (nil for all). +function M.set_pos(tgt) + for t, win in pairs(ui.wins) do + local cfg = (t == tgt or (tgt == nil and t ~= 'cmd')) + and api.nvim_win_is_valid(win) + and api.nvim_win_get_config(win) + if cfg and (tgt or not cfg.hide) then + local texth = api.nvim_win_text_height(win, { max_height = o.lines }) + local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' } + cfg = { hide = false, relative = 'laststatus', col = 10000 } ---@type table + cfg.row, cfg.height, cfg.border = win_row_height_border(t, texth.all) + cfg.border = cfg.border and t ~= 'msg' and { '', top, '', '', '', '', '', '' } or nil + cfg.mouse = tgt == 'cmd' or t == 'msg' or nil + api.nvim_win_set_config(win, cfg) + api.nvim_win_set_cursor(win, { 1, 0 }) + + if tgt == 'cmd' then + -- Dismiss temporarily expanded cmdline on next keypress and update spill indicator. + texth.all = math.max(texth.all, api.nvim_buf_line_count(ui.bufs.cmd)) + local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height) + M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil + set_virttext('cmd', 'cmd') + M.virt.cmd[M.virt.idx.spill][1] = { 0, (' [+%d]'):format(texth.all - ui.cmdheight) } + M.cmd_on_key = vim.on_key(cmd_on_key, ui.ns) + elseif tgt == 'dialog' and set_top_bot_spill() then + M.dialog_on_key = vim.on_key(dialog_on_key, M.dialog_on_key) + elseif tgt == 'msg' then + -- Ensure last line is visible and first line is at top of window. + api.nvim_win_set_cursor(ui.wins.msg, { api.nvim_buf_line_count(ui.bufs.msg), 0 }) + elseif tgt == 'pager' and not in_pager then + enter_pager() + end + end + end +end + +return M diff --git a/tests/testdata/lua_neovim_write_spec_refactor/expected.json b/tests/testdata/lua_neovim_write_spec_refactor/expected.json new file mode 100644 index 0000000..b7e909c --- /dev/null +++ b/tests/testdata/lua_neovim_write_spec_refactor/expected.json @@ -0,0 +1,3707 @@ +{ + "version": "v1", + "actions": [ + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27, + 3 + ], + "type": "block", + "start_byte": 4275, + "end_byte": 4557 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 2821, + "end_byte": 4639 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27 + ], + "type": "if_statement", + "start_byte": 4244, + "end_byte": 4565 + }, + "old_position": 3, + "subtree": true, + "dest_start_byte": 2836, + "dest_end_byte": 4633 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 0 + ], + "type": "function_call", + "start_byte": 2836, + "end_byte": 2876 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 2836, + "dest_end_byte": 2876 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 1 + ], + "type": "function_call", + "start_byte": 2881, + "end_byte": 2911 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 2881, + "dest_end_byte": 2911, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 2 + ], + "type": "function_call", + "start_byte": 2916, + "end_byte": 2956 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 2, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 2, + "subtree": true, + "dest_start_byte": 2916, + "dest_end_byte": 2956, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 3 + ], + "type": "function_call", + "start_byte": 2961, + "end_byte": 2983 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 3, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 3, + "subtree": true, + "dest_start_byte": 2961, + "dest_end_byte": 2983, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 4 + ], + "type": "function_call", + "start_byte": 2989, + "end_byte": 3029 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 4, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 4, + "subtree": true, + "dest_start_byte": 2989, + "dest_end_byte": 3029, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 5 + ], + "type": "function_call", + "start_byte": 3034, + "end_byte": 3066 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 5, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 5, + "subtree": true, + "dest_start_byte": 3034, + "dest_end_byte": 3066, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 6 + ], + "type": "function_call", + "start_byte": 3071, + "end_byte": 3111 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 6, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 6, + "subtree": true, + "dest_start_byte": 3071, + "dest_end_byte": 3111, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 7 + ], + "type": "function_call", + "start_byte": 3116, + "end_byte": 3138 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 7, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 7, + "subtree": true, + "dest_start_byte": 3116, + "dest_end_byte": 3138, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 8 + ], + "type": "function_call", + "start_byte": 3144, + "end_byte": 3202 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 8, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 8, + "subtree": true, + "dest_start_byte": 3144, + "dest_end_byte": 3202, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 9 + ], + "type": "function_call", + "start_byte": 3207, + "end_byte": 3255 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 9, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 9, + "subtree": true, + "dest_start_byte": 3207, + "dest_end_byte": 3255, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 10 + ], + "type": "function_call", + "start_byte": 3260, + "end_byte": 3318 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 10, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 10, + "subtree": true, + "dest_start_byte": 3260, + "dest_end_byte": 3318, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 11 + ], + "type": "function_call", + "start_byte": 3324, + "end_byte": 3383 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 11, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 11, + "subtree": true, + "dest_start_byte": 3324, + "dest_end_byte": 3383, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 12 + ], + "type": "function_call", + "start_byte": 3388, + "end_byte": 3448 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 12, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 12, + "subtree": true, + "dest_start_byte": 3388, + "dest_end_byte": 3448, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 13 + ], + "type": "function_call", + "start_byte": 3453, + "end_byte": 3513 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 13, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 13, + "subtree": true, + "dest_start_byte": 3453, + "dest_end_byte": 3513, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 14 + ], + "type": "function_call", + "start_byte": 3518, + "end_byte": 3562 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 14, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 14, + "subtree": true, + "dest_start_byte": 3518, + "dest_end_byte": 3562, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 15 + ], + "type": "function_call", + "start_byte": 3567, + "end_byte": 3590 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 15, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 15, + "subtree": true, + "dest_start_byte": 3567, + "dest_end_byte": 3590, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 16 + ], + "type": "function_call", + "start_byte": 3595, + "end_byte": 3638 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 16, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 16, + "subtree": true, + "dest_start_byte": 3595, + "dest_end_byte": 3638, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 17 + ], + "type": "function_call", + "start_byte": 3643, + "end_byte": 3666 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 17, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 17, + "subtree": true, + "dest_start_byte": 3643, + "dest_end_byte": 3666, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 18 + ], + "type": "function_call", + "start_byte": 3671, + "end_byte": 3714 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 18, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 18, + "subtree": true, + "dest_start_byte": 3671, + "dest_end_byte": 3714, + "group_id": "group-1" + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 19 + ], + "type": "comment", + "start_byte": 3719, + "end_byte": 3777 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 19 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 20 + ], + "type": "comment", + "start_byte": 3782, + "end_byte": 3815 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 20 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 21 + ], + "type": "function_call", + "start_byte": 3820, + "end_byte": 3839 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 21, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 21, + "subtree": true, + "dest_start_byte": 3820, + "dest_end_byte": 3839, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 22 + ], + "type": "function_call", + "start_byte": 3844, + "end_byte": 3903 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 22, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 22, + "subtree": true, + "dest_start_byte": 3844, + "dest_end_byte": 3903, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 23 + ], + "type": "function_call", + "start_byte": 3908, + "end_byte": 3968 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 23, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 23, + "subtree": true, + "dest_start_byte": 3908, + "dest_end_byte": 3968, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 24 + ], + "type": "function_call", + "start_byte": 3973, + "end_byte": 4033 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 24, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 24, + "subtree": true, + "dest_start_byte": 3973, + "dest_end_byte": 4033, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 25 + ], + "type": "function_call", + "start_byte": 4038, + "end_byte": 4155 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 25, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 25, + "subtree": true, + "dest_start_byte": 4038, + "dest_end_byte": 4155, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 26 + ], + "type": "function_call", + "start_byte": 4161, + "end_byte": 4239 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 26, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 26, + "subtree": true, + "dest_start_byte": 4161, + "dest_end_byte": 4239, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 28 + ], + "type": "function_call", + "start_byte": 4571, + "end_byte": 4686 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4633 + }, + "position": 29, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + }, + "old_position": 28, + "subtree": true, + "dest_start_byte": 4518, + "dest_end_byte": 4633, + "group_id": "group-1" + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 3, + 0 + ], + "type": "function_call", + "start_byte": 4901, + "end_byte": 5034 + }, + "parent": { + "tree": "after", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 4686, + "end_byte": 6124 + }, + "position": 3, + "old_parent": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 3 + ], + "type": "block", + "start_byte": 4901, + "end_byte": 5034 + }, + "old_position": 0, + "subtree": true, + "dest_start_byte": 4817, + "dest_end_byte": 4944 + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 19 + ], + "type": "comment", + "start_byte": 3719, + "end_byte": 3777 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 20 + ], + "type": "comment", + "start_byte": 3782, + "end_byte": 3815 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 4244, + "end_byte": 4246 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27, + 1 + ], + "type": "unary_expression", + "start_byte": 4247, + "end_byte": 4263 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 4264, + "end_byte": 4268 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27, + 4 + ], + "type": "end", + "label": "end", + "start_byte": 4562, + "end_byte": 4565 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2, + 27 + ], + "type": "if_statement", + "start_byte": 4244, + "end_byte": 4275 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 6, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 2836, + "end_byte": 4686 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 0 + ], + "type": "if", + "label": "if", + "start_byte": 4870, + "end_byte": 4872 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 1 + ], + "type": "unary_expression", + "start_byte": 4873, + "end_byte": 4889 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 2 + ], + "type": "then", + "label": "then", + "start_byte": 4890, + "end_byte": 4894 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 3 + ], + "type": "block", + "start_byte": 4901, + "end_byte": 5034 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3, + 4 + ], + "type": "end", + "label": "end", + "start_byte": 5039, + "end_byte": 5042 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 14, + 1, + 1, + 2, + 7, + 1, + 1, + 2, + 3 + ], + "type": "if_statement", + "start_byte": 4870, + "end_byte": 4901 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": -1 + }, + { + "left_line": 97, + "right_line": -1 + }, + { + "left_line": 98, + "right_line": -1 + }, + { + "left_line": 99, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 96 + }, + { + "left_line": -1, + "right_line": 97 + }, + { + "left_line": -1, + "right_line": 98 + }, + { + "left_line": -1, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": -1 + }, + { + "left_line": 102, + "right_line": -1 + }, + { + "left_line": 103, + "right_line": -1 + }, + { + "left_line": 104, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 101 + }, + { + "left_line": -1, + "right_line": 102 + }, + { + "left_line": -1, + "right_line": 103 + }, + { + "left_line": -1, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": -1 + }, + { + "left_line": 107, + "right_line": -1 + }, + { + "left_line": 108, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 106 + }, + { + "left_line": -1, + "right_line": 107 + }, + { + "left_line": -1, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": -1 + }, + { + "left_line": 111, + "right_line": -1 + }, + { + "left_line": 112, + "right_line": -1 + }, + { + "left_line": 113, + "right_line": -1 + }, + { + "left_line": 114, + "right_line": -1 + }, + { + "left_line": 115, + "right_line": -1 + }, + { + "left_line": 116, + "right_line": -1 + }, + { + "left_line": 117, + "right_line": -1 + }, + { + "left_line": 118, + "right_line": -1 + }, + { + "left_line": 119, + "right_line": -1 + }, + { + "left_line": 120, + "right_line": -1 + }, + { + "left_line": 121, + "right_line": -1 + }, + { + "left_line": 122, + "right_line": -1 + }, + { + "left_line": 123, + "right_line": -1 + }, + { + "left_line": 124, + "right_line": -1 + }, + { + "left_line": 125, + "right_line": -1 + }, + { + "left_line": 126, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 110 + }, + { + "left_line": -1, + "right_line": 111 + }, + { + "left_line": -1, + "right_line": 112 + }, + { + "left_line": -1, + "right_line": 113 + }, + { + "left_line": -1, + "right_line": 114 + }, + { + "left_line": -1, + "right_line": 115 + }, + { + "left_line": -1, + "right_line": 116 + }, + { + "left_line": -1, + "right_line": 117 + }, + { + "left_line": -1, + "right_line": 118 + }, + { + "left_line": -1, + "right_line": 119 + }, + { + "left_line": -1, + "right_line": 120 + }, + { + "left_line": -1, + "right_line": 121 + }, + { + "left_line": -1, + "right_line": 122 + }, + { + "left_line": -1, + "right_line": 123 + }, + { + "left_line": -1, + "right_line": 124 + }, + { + "left_line": -1, + "right_line": 125 + }, + { + "left_line": -1, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": -1 + }, + { + "left_line": 130, + "right_line": -1 + }, + { + "left_line": 131, + "right_line": -1 + }, + { + "left_line": 132, + "right_line": -1 + }, + { + "left_line": 133, + "right_line": -1 + }, + { + "left_line": 134, + "right_line": -1 + }, + { + "left_line": 135, + "right_line": -1 + }, + { + "left_line": 136, + "right_line": -1 + }, + { + "left_line": 137, + "right_line": -1 + }, + { + "left_line": 138, + "right_line": -1 + }, + { + "left_line": 139, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 129 + }, + { + "left_line": -1, + "right_line": 130 + }, + { + "left_line": -1, + "right_line": 131 + }, + { + "left_line": -1, + "right_line": 132 + }, + { + "left_line": -1, + "right_line": 133 + }, + { + "left_line": -1, + "right_line": 134 + }, + { + "left_line": -1, + "right_line": 135 + }, + { + "left_line": -1, + "right_line": 136 + }, + { + "left_line": -1, + "right_line": 137 + }, + { + "left_line": 140, + "right_line": 138 + }, + { + "left_line": 141, + "right_line": -1 + }, + { + "left_line": 142, + "right_line": -1 + }, + { + "left_line": 143, + "right_line": -1 + }, + { + "left_line": -1, + "right_line": 139 + }, + { + "left_line": -1, + "right_line": 140 + }, + { + "left_line": -1, + "right_line": 141 + }, + { + "left_line": 144, + "right_line": 142 + }, + { + "left_line": 145, + "right_line": 143 + }, + { + "left_line": 146, + "right_line": 144 + }, + { + "left_line": 147, + "right_line": 145 + }, + { + "left_line": 148, + "right_line": 146 + }, + { + "left_line": 149, + "right_line": 147 + }, + { + "left_line": 150, + "right_line": 148 + }, + { + "left_line": 151, + "right_line": -1 + }, + { + "left_line": 152, + "right_line": 149 + }, + { + "left_line": 153, + "right_line": 150 + }, + { + "left_line": 154, + "right_line": 151 + }, + { + "left_line": 155, + "right_line": 152 + }, + { + "left_line": 156, + "right_line": -1 + }, + { + "left_line": 157, + "right_line": 153 + }, + { + "left_line": 158, + "right_line": 154 + }, + { + "left_line": 159, + "right_line": 155 + }, + { + "left_line": 160, + "right_line": 156 + }, + { + "left_line": 161, + "right_line": 157 + }, + { + "left_line": 162, + "right_line": 158 + }, + { + "left_line": 163, + "right_line": 159 + }, + { + "left_line": 164, + "right_line": 160 + }, + { + "left_line": 165, + "right_line": 161 + }, + { + "left_line": 166, + "right_line": 162 + }, + { + "left_line": 167, + "right_line": 163 + }, + { + "left_line": 168, + "right_line": 164 + }, + { + "left_line": 169, + "right_line": 165 + }, + { + "left_line": 170, + "right_line": 166 + }, + { + "left_line": 171, + "right_line": 167 + }, + { + "left_line": 172, + "right_line": 168 + }, + { + "left_line": 173, + "right_line": 169 + }, + { + "left_line": 174, + "right_line": 170 + }, + { + "left_line": 175, + "right_line": 171 + }, + { + "left_line": 176, + "right_line": 172 + }, + { + "left_line": 177, + "right_line": 173 + }, + { + "left_line": 178, + "right_line": 174 + }, + { + "left_line": 179, + "right_line": 175 + }, + { + "left_line": 180, + "right_line": 176 + }, + { + "left_line": 181, + "right_line": 177 + }, + { + "left_line": 182, + "right_line": 178 + }, + { + "left_line": 183, + "right_line": 179 + }, + { + "left_line": 184, + "right_line": 180 + }, + { + "left_line": 185, + "right_line": 181 + }, + { + "left_line": 186, + "right_line": 182 + }, + { + "left_line": 187, + "right_line": 183 + }, + { + "left_line": 188, + "right_line": 184 + }, + { + "left_line": 189, + "right_line": 185 + }, + { + "left_line": 190, + "right_line": 186 + }, + { + "left_line": 191, + "right_line": 187 + }, + { + "left_line": 192, + "right_line": 188 + }, + { + "left_line": 193, + "right_line": 189 + }, + { + "left_line": 194, + "right_line": 190 + }, + { + "left_line": 195, + "right_line": 191 + }, + { + "left_line": 196, + "right_line": 192 + }, + { + "left_line": 197, + "right_line": 193 + }, + { + "left_line": 198, + "right_line": 194 + }, + { + "left_line": 199, + "right_line": 195 + }, + { + "left_line": 200, + "right_line": 196 + }, + { + "left_line": 201, + "right_line": 197 + }, + { + "left_line": 202, + "right_line": 198 + }, + { + "left_line": 203, + "right_line": 199 + }, + { + "left_line": 204, + "right_line": 200 + }, + { + "left_line": 205, + "right_line": 201 + }, + { + "left_line": 206, + "right_line": 202 + }, + { + "left_line": 207, + "right_line": 203 + }, + { + "left_line": 208, + "right_line": 204 + }, + { + "left_line": 209, + "right_line": 205 + }, + { + "left_line": 210, + "right_line": 206 + }, + { + "left_line": 211, + "right_line": 207 + }, + { + "left_line": 212, + "right_line": 208 + }, + { + "left_line": 213, + "right_line": 209 + }, + { + "left_line": 214, + "right_line": 210 + }, + { + "left_line": 215, + "right_line": 211 + }, + { + "left_line": 216, + "right_line": 212 + }, + { + "left_line": 217, + "right_line": 213 + }, + { + "left_line": 218, + "right_line": 214 + }, + { + "left_line": 219, + "right_line": 215 + }, + { + "left_line": 220, + "right_line": 216 + }, + { + "left_line": 221, + "right_line": 217 + }, + { + "left_line": 222, + "right_line": 218 + }, + { + "left_line": 223, + "right_line": 219 + }, + { + "left_line": 224, + "right_line": 220 + }, + { + "left_line": 225, + "right_line": 221 + }, + { + "left_line": 226, + "right_line": 222 + }, + { + "left_line": 227, + "right_line": 223 + }, + { + "left_line": 228, + "right_line": 224 + }, + { + "left_line": 229, + "right_line": 225 + }, + { + "left_line": 230, + "right_line": 226 + }, + { + "left_line": 231, + "right_line": 227 + }, + { + "left_line": 232, + "right_line": 228 + }, + { + "left_line": 233, + "right_line": 229 + }, + { + "left_line": 234, + "right_line": 230 + }, + { + "left_line": 235, + "right_line": 231 + }, + { + "left_line": 236, + "right_line": 232 + }, + { + "left_line": 237, + "right_line": 233 + }, + { + "left_line": 238, + "right_line": 234 + }, + { + "left_line": 239, + "right_line": 235 + }, + { + "left_line": 240, + "right_line": 236 + }, + { + "left_line": 241, + "right_line": 237 + }, + { + "left_line": 242, + "right_line": 238 + }, + { + "left_line": 243, + "right_line": 239 + }, + { + "left_line": 244, + "right_line": 240 + }, + { + "left_line": 245, + "right_line": 241 + }, + { + "left_line": 246, + "right_line": 242 + }, + { + "left_line": 247, + "right_line": 243 + }, + { + "left_line": 248, + "right_line": 244 + }, + { + "left_line": 249, + "right_line": 245 + }, + { + "left_line": 250, + "right_line": 246 + }, + { + "left_line": 251, + "right_line": 247 + }, + { + "left_line": 252, + "right_line": 248 + }, + { + "left_line": 253, + "right_line": 249 + }, + { + "left_line": 254, + "right_line": 250 + }, + { + "left_line": 255, + "right_line": 251 + }, + { + "left_line": 256, + "right_line": 252 + }, + { + "left_line": 257, + "right_line": 253 + }, + { + "left_line": 258, + "right_line": 254 + }, + { + "left_line": 259, + "right_line": 255 + }, + { + "left_line": 260, + "right_line": 256 + }, + { + "left_line": 261, + "right_line": 257 + }, + { + "left_line": 262, + "right_line": 258 + }, + { + "left_line": 263, + "right_line": 259 + }, + { + "left_line": 264, + "right_line": 260 + }, + { + "left_line": 265, + "right_line": 261 + }, + { + "left_line": 266, + "right_line": 262 + }, + { + "left_line": 267, + "right_line": 263 + }, + { + "left_line": 268, + "right_line": 264 + }, + { + "left_line": 269, + "right_line": 265 + }, + { + "left_line": 270, + "right_line": 266 + }, + { + "left_line": 271, + "right_line": 267 + }, + { + "left_line": 272, + "right_line": 268 + }, + { + "left_line": 273, + "right_line": 269 + }, + { + "left_line": 274, + "right_line": 270 + }, + { + "left_line": 275, + "right_line": 271 + }, + { + "left_line": 276, + "right_line": 272 + }, + { + "left_line": 277, + "right_line": 273 + }, + { + "left_line": 278, + "right_line": 274 + }, + { + "left_line": 279, + "right_line": 275 + }, + { + "left_line": 280, + "right_line": 276 + }, + { + "left_line": 281, + "right_line": 277 + }, + { + "left_line": 282, + "right_line": 278 + }, + { + "left_line": 283, + "right_line": 279 + }, + { + "left_line": 284, + "right_line": 280 + }, + { + "left_line": 285, + "right_line": 281 + }, + { + "left_line": 286, + "right_line": 282 + }, + { + "left_line": 287, + "right_line": 283 + }, + { + "left_line": 288, + "right_line": 284 + }, + { + "left_line": 289, + "right_line": 285 + }, + { + "left_line": 290, + "right_line": 286 + }, + { + "left_line": 291, + "right_line": 287 + }, + { + "left_line": 292, + "right_line": 288 + }, + { + "left_line": 293, + "right_line": 289 + }, + { + "left_line": 294, + "right_line": 290 + }, + { + "left_line": 295, + "right_line": 291 + }, + { + "left_line": 296, + "right_line": 292 + }, + { + "left_line": 297, + "right_line": 293 + }, + { + "left_line": 298, + "right_line": 294 + }, + { + "left_line": 299, + "right_line": 295 + }, + { + "left_line": 300, + "right_line": 296 + }, + { + "left_line": 301, + "right_line": 297 + }, + { + "left_line": 302, + "right_line": 298 + }, + { + "left_line": 303, + "right_line": 299 + }, + { + "left_line": 304, + "right_line": 300 + }, + { + "left_line": 305, + "right_line": 301 + }, + { + "left_line": 306, + "right_line": 302 + }, + { + "left_line": 307, + "right_line": 303 + }, + { + "left_line": 308, + "right_line": 304 + }, + { + "left_line": 309, + "right_line": 305 + }, + { + "left_line": 310, + "right_line": 306 + }, + { + "left_line": 311, + "right_line": 307 + }, + { + "left_line": 312, + "right_line": 308 + }, + { + "left_line": 313, + "right_line": 309 + }, + { + "left_line": 314, + "right_line": 310 + }, + { + "left_line": 315, + "right_line": 311 + }, + { + "left_line": 316, + "right_line": 312 + }, + { + "left_line": 317, + "right_line": 313 + }, + { + "left_line": 318, + "right_line": 314 + }, + { + "left_line": 319, + "right_line": 315 + }, + { + "left_line": 320, + "right_line": 316 + }, + { + "left_line": 321, + "right_line": 317 + }, + { + "left_line": 322, + "right_line": 318 + }, + { + "left_line": 323, + "right_line": 319 + }, + { + "left_line": 324, + "right_line": 320 + }, + { + "left_line": 325, + "right_line": 321 + }, + { + "left_line": 326, + "right_line": 322 + }, + { + "left_line": 327, + "right_line": 323 + }, + { + "left_line": 328, + "right_line": 324 + }, + { + "left_line": 329, + "right_line": 325 + }, + { + "left_line": 330, + "right_line": 326 + }, + { + "left_line": 331, + "right_line": 327 + }, + { + "left_line": 332, + "right_line": 328 + }, + { + "left_line": 333, + "right_line": 329 + }, + { + "left_line": 334, + "right_line": 330 + }, + { + "left_line": 335, + "right_line": 331 + }, + { + "left_line": 336, + "right_line": 332 + }, + { + "left_line": 337, + "right_line": 333 + }, + { + "left_line": 338, + "right_line": 334 + }, + { + "left_line": 339, + "right_line": 335 + }, + { + "left_line": 340, + "right_line": 336 + }, + { + "left_line": 341, + "right_line": 337 + }, + { + "left_line": 342, + "right_line": 338 + }, + { + "left_line": 343, + "right_line": 339 + }, + { + "left_line": 344, + "right_line": 340 + }, + { + "left_line": 345, + "right_line": 341 + }, + { + "left_line": 346, + "right_line": 342 + }, + { + "left_line": 347, + "right_line": 343 + }, + { + "left_line": 348, + "right_line": 344 + }, + { + "left_line": 349, + "right_line": 345 + }, + { + "left_line": 350, + "right_line": 346 + }, + { + "left_line": 351, + "right_line": 347 + }, + { + "left_line": 352, + "right_line": 348 + }, + { + "left_line": 353, + "right_line": 349 + }, + { + "left_line": 354, + "right_line": 350 + }, + { + "left_line": 355, + "right_line": 351 + }, + { + "left_line": 356, + "right_line": 352 + }, + { + "left_line": 357, + "right_line": 353 + }, + { + "left_line": 358, + "right_line": 354 + }, + { + "left_line": 359, + "right_line": 355 + }, + { + "left_line": 360, + "right_line": 356 + }, + { + "left_line": 361, + "right_line": 357 + }, + { + "left_line": 362, + "right_line": 358 + }, + { + "left_line": 363, + "right_line": 359 + }, + { + "left_line": 364, + "right_line": 360 + }, + { + "left_line": 365, + "right_line": 361 + }, + { + "left_line": 366, + "right_line": 362 + }, + { + "left_line": 367, + "right_line": 363 + }, + { + "left_line": 368, + "right_line": 364 + }, + { + "left_line": 369, + "right_line": 365 + }, + { + "left_line": 370, + "right_line": 366 + }, + { + "left_line": 371, + "right_line": 367 + }, + { + "left_line": 372, + "right_line": 368 + }, + { + "left_line": 373, + "right_line": 369 + }, + { + "left_line": 374, + "right_line": 370 + }, + { + "left_line": 375, + "right_line": 371 + }, + { + "left_line": 376, + "right_line": 372 + }, + { + "left_line": 377, + "right_line": 373 + }, + { + "left_line": 378, + "right_line": 374 + }, + { + "left_line": 379, + "right_line": 375 + }, + { + "left_line": 380, + "right_line": 376 + }, + { + "left_line": 381, + "right_line": 377 + }, + { + "left_line": 382, + "right_line": 378 + }, + { + "left_line": 383, + "right_line": 379 + }, + { + "left_line": 384, + "right_line": 380 + }, + { + "left_line": 385, + "right_line": 381 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_write_spec_refactor/metadata.json b/tests/testdata/lua_neovim_write_spec_refactor/metadata.json new file mode 100644 index 0000000..e3c7baa --- /dev/null +++ b/tests/testdata/lua_neovim_write_spec_refactor/metadata.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "7ba955fe079d4aa2554fea8e7235651fafd40efb", + "commit": "689b45dd", + "message": "fix(write): show E17 on all platforms #40595", + "author": "Willaaaaaaa", + "date": "2026-07-20T03:42:59+08:00", + "language": "lua", + "files_changed": 1, + "insertions": 12, + "deletions": 16 +} diff --git a/tests/testdata/lua_neovim_write_spec_refactor/new.lua b/tests/testdata/lua_neovim_write_spec_refactor/new.lua new file mode 100644 index 0000000..003a191 --- /dev/null +++ b/tests/testdata/lua_neovim_write_spec_refactor/new.lua @@ -0,0 +1,381 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local eq, eval, clear, write_file, source, insert = + t.eq, n.eval, n.clear, t.write_file, n.source, n.insert +local pcall_err = t.pcall_err +local command = n.command +local feed_command = n.feed_command +local fn = n.fn +local api = n.api +local skip = t.skip +local is_os = t.is_os +local read_file = t.read_file + +local fname = 'Xtest-functional-ex_cmds-write' +local fname_bak = fname .. '~' +local fname_broken = fname_bak .. 'broken' + +describe(':write', function() + local function cleanup() + n.rmdir('Xtest_write') + os.remove('Xtest_bkc_file.txt') + os.remove('Xtest_bkc_link.txt') + os.remove('Xtest_fifo') + os.remove(fname) + os.remove(fname_bak) + os.remove(fname_broken) + end + before_each(function() + clear() + cleanup() + end) + after_each(function() + cleanup() + end) + + it('&backupcopy=auto preserves symlinks', function() + command('set backupcopy=auto') + write_file('Xtest_bkc_file.txt', 'content0') + if is_os('win') then + command('silent !mklink Xtest_bkc_link.txt Xtest_bkc_file.txt') + else + command('silent !ln -s Xtest_bkc_file.txt Xtest_bkc_link.txt') + end + if eval('v:shell_error') ~= 0 then + pending('Cannot create symlink') + end + source([[ + edit Xtest_bkc_link.txt + call setline(1, ['content1']) + write + ]]) + eq(eval("['content1']"), eval("readfile('Xtest_bkc_file.txt')")) + eq(eval("['content1']"), eval("readfile('Xtest_bkc_link.txt')")) + end) + + it('&backupcopy=no replaces symlink with new file', function() + command('set backupcopy=no') + write_file('Xtest_bkc_file.txt', 'content0') + if is_os('win') then + command('silent !mklink Xtest_bkc_link.txt Xtest_bkc_file.txt') + else + command('silent !ln -s Xtest_bkc_file.txt Xtest_bkc_link.txt') + end + if eval('v:shell_error') ~= 0 then + pending('Cannot create symlink') + end + source([[ + edit Xtest_bkc_link.txt + call setline(1, ['content1']) + write + ]]) + eq(eval("['content0']"), eval("readfile('Xtest_bkc_file.txt')")) + eq(eval("['content1']"), eval("readfile('Xtest_bkc_link.txt')")) + end) + + it('appends FIFO file', function() + -- mkfifo creates read-only .lnk files on Windows + if is_os('win') or eval("executable('mkfifo')") == 0 then + pending('N/A: missing "mkfifo" command') + end + + local text = 'some fifo text from write_spec' + assert(os.execute('mkfifo Xtest_fifo')) + insert(text) + + -- Blocks until a consumer reads the FIFO. + feed_command('write >> Xtest_fifo') + + -- Read the FIFO, this will unblock the :write above. + local fifo = assert(io.open('Xtest_fifo')) + eq(text .. '\n', fifo:read('*all')) + fifo:close() + end) + + it('++p creates missing parent directories', function() + eq(0, eval("filereadable('p_opt.txt')")) + command('write ++p p_opt.txt') + eq(1, eval("filereadable('p_opt.txt')")) + os.remove('p_opt.txt') + + eq(0, eval("filereadable('p_opt.txt')")) + command('write ++p ./p_opt.txt') + eq(1, eval("filereadable('p_opt.txt')")) + os.remove('p_opt.txt') + + eq(0, eval("filereadable('Xtest_write/write/p_opt.txt')")) + command('write ++p Xtest_write/write/p_opt.txt') + eq(1, eval("filereadable('Xtest_write/write/p_opt.txt')")) + + eq(0, eval("filereadable('Xtest_write/write2/p_opt.txt')")) + eq(0, eval("filereadable('Xtest_write/write2/p_opt2.txt')")) + eq(0, eval("filereadable('Xtest_write/write3/p_opt3.txt')")) + command('file Xtest_write/write2/p_opt.txt') + command('set modified') + command('sp Xtest_write/write2/p_opt2.txt') + command('set modified') + command('sp Xtest_write/write3/p_opt3.txt') + -- don't set p_opt3.txt modified - assert it isn't written + -- and that write3/ isn't created + command('wall ++p') + eq(1, eval("filereadable('Xtest_write/write2/p_opt.txt')")) + eq(1, eval("filereadable('Xtest_write/write2/p_opt2.txt')")) + eq(0, eval("filereadable('Xtest_write/write3/p_opt3.txt')")) + t.matches( + 'E474: Invalid argument', + pcall_err(command, 'read ++edits Xtest_write/write/p_opt.txt') + ) + + eq('Vim(write):E32: No file name', pcall_err(command, 'write ++p Xotherdir/')) + eq( + ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), + pcall_err(command, 'write ++p .') + ) + eq( + ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), + pcall_err(command, 'write ++p ./') + ) + + t.matches( + 'E474: Invalid argument', + pcall_err(command, 'write ++patate Xtest_write/garbage.txt') + ) + end) + + it('errors out correctly', function() + command('let $HOME=""') + eq(fn.fnamemodify('.', ':p:h'), fn.fnamemodify('.', ':p:h:~')) + -- Message from check_overwrite + eq( + ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), + pcall_err(command, 'write .') + ) + api.nvim_set_option_value('writeany', true, {}) + -- Message from buf_write + eq('Vim(write):E502: "." is a directory', pcall_err(command, 'write .')) + fn.mkdir(fname_bak) + api.nvim_set_option_value('backupdir', '.', {}) + api.nvim_set_option_value('backup', true, {}) + write_file(fname, 'content0') + command('edit ' .. fname) + fn.setline(1, 'TTY') + eq("Vim(write):E510: Can't make backup file (add ! to override)", pcall_err(command, 'write')) + api.nvim_set_option_value('backup', false, {}) + fn.setfperm(fname, 'r--------') + eq( + 'Vim(write):E505: "Xtest-functional-ex_cmds-write" is read-only (add ! to override)', + pcall_err(command, 'write') + ) + if is_os('win') then + eq(0, os.execute('del /q/f ' .. fname)) + eq(0, os.execute('rd /q/s ' .. fname_bak)) + else + eq(true, os.remove(fname)) + eq(true, os.remove(fname_bak)) + end + write_file(fname_bak, 'TTYX') + skip(is_os('win'), [[FIXME: pcall_err(command, 'write!') outputs 0 in Windows]]) + vim.uv.fs_symlink(fname_bak .. ('/xxxxx'):rep(20), fname) + eq("Vim(write):E166: Can't open linked file for writing", pcall_err(command, 'write!')) + end) + + it('fails converting a trailing incomplete sequence', function() + -- From https://github.com/neovim/neovim/issues/36990, an invalid UTF-8 sequence at the end of + -- the file during conversion testing can overwrite the rest of the file during the real + -- conversion. + + api.nvim_buf_set_lines(0, 0, 1, true, { 'line 1', 'line 2', 'aaabbb\235\128' }) + command('set noendofline nofixendofline') + + eq( + "Vim(write):E513: Write error, conversion failed in line 3 (make 'fenc' empty to override)", + pcall_err(command, 'write ++enc=latin1 ' .. fname) + ) + end) + + it('converts to latin1 with an invalid sequence at buffer boundary', function() + -- From https://github.com/neovim/neovim/issues/36990, an invalid UTF-8 sequence that falls + -- right at the end of the 8 KiB buffer used for encoding conversions causes subsequent data to + -- be overwritten. + + local content = string.rep('a', 1024 * 8 - 1) .. '\251' .. string.rep('b', 20) + api.nvim_buf_set_lines(0, 0, 1, true, { content }) + command('set noendofline nofixendofline fenc=latin1') + command('write ' .. fname) + + local tail = string.sub(read_file(fname) or '', -10) + eq('bbbbbbbbbb', tail) + end) + + it('converts to CP1251 with iconv', function() + api.nvim_buf_set_lines( + 0, + 0, + 1, + true, + { 'Привет, мир!', 'Это простой тест.' } + ) + command('write ++enc=cp1251 ++ff=unix ' .. fname) + + eq( + '\207\240\232\226\229\242, \236\232\240!\n' + .. '\221\242\238 \239\240\238\241\242\238\233 \242\229\241\242.\n', + read_file(fname) + ) + end) + + it('converts to GB18030 with iconv', function() + api.nvim_buf_set_lines(0, 0, 1, true, { '你好,世界!', '这是一个测试。' }) + command('write ++enc=gb18030 ++ff=unix ' .. fname) + + eq( + '\196\227\186\195\163\172\202\192\189\231\163\161\n' + .. '\213\226\202\199\210\187\184\246\178\226\202\212\161\163\n', + read_file(fname) + ) + end) + + it('converts to Shift_JIS with iconv', function() + api.nvim_buf_set_lines( + 0, + 0, + 1, + true, + { 'こんにちは、世界!', 'これはテストです。' } + ) + command('write ++enc=sjis ++ff=unix ' .. fname) + + eq( + '\130\177\130\241\130\201\130\191\130\205\129A\144\162\138E\129I\n' + .. '\130\177\130\234\130\205\131e\131X\131g\130\197\130\183\129B\n', + read_file(fname) + ) + end) + + it('fails converting an illegal sequence with iconv', function() + api.nvim_buf_set_lines(0, 0, 1, true, { 'line 1', 'aaa\128bbb' }) + + eq( + "Vim(write):E513: Write error, conversion failed (make 'fenc' empty to override)", + pcall_err(command, 'write ++enc=cp1251 ' .. fname) + ) + end) + + it('handles a multi-byte sequence crossing the buffer boundary converting with iconv', function() + local content = string.rep('a', 1024 * 8 - 1) .. 'Дbbbbb' + api.nvim_buf_set_lines(0, 0, 1, true, { content }) + -- Skip the backup so we're testing the "checking" phase also. + command('set nowritebackup') + command('write ++enc=cp1251 ++ff=unix ' .. fname) + + local expected = string.rep('a', 1024 * 8 - 1) .. '\196bbbbb\n' + eq(expected, read_file(fname)) + end) +end) + +describe(':update', function() + before_each(function() + clear() + fn.mkdir('Xtest_update', 'p') + end) + + after_each(function() + fn.delete('Xtest_update', 'rf') + end) + + it('works for a new buffer', function() + command('edit Xtest_update/foo/bar/nonexist.taz | update ++p') + eq(1, eval("filereadable('Xtest_update/foo/bar/nonexist.taz')")) + end) + + it('writes modified buffer', function() + command('edit Xtest_update/modified.txt') + command('call setline(1, "hello world")') + command('update') + eq({ 'hello world' }, fn.readfile('Xtest_update/modified.txt')) + end) + + it('does not write unmodified existing file', function() + local filename = 'Xtest_update/existing.txt' + local fd = io.open(filename, 'w') + fd:write('content') + fd:close() + local mtime_before = fn.getftime(filename) + command('edit ' .. filename) + command('update') + eq(mtime_before, fn.getftime(filename)) + end) + + it('creates parent directories with ++p', function() + command('edit Xtest_update/deep/nested/path/file.txt') + command('call setline(1, "content")') + command('update ++p') + eq(1, eval("filereadable('Xtest_update/deep/nested/path/file.txt')")) + end) + + it('fails gracefully for unnamed buffer', function() + command('enew') + command('call setline(1, "some content")') + eq('Vim(update):E32: No file name', pcall_err(command, 'update')) + end) + + it('respects readonly files', function() + local filename = 'Xtest_update/readonly.txt' + local fd = io.open(filename, 'w') + fd:write('readonly content') + fd:close() + command('edit ' .. filename) + command('set readonly') + command('call setline(1, "modified")') + eq( + "Vim(update):E45: 'readonly' option is set (add ! to override)", + pcall_err(command, 'update') + ) + + command('update!') + eq({ 'modified' }, fn.readfile('Xtest_update/readonly.txt')) + end) + + it('can write line ranges', function() + command('edit Xtest_update/range.txt') + command('call setline(1, ["line1", "line2", "line3", "line4"])') + command('2,3update!') + eq({ 'line2', 'line3' }, fn.readfile('Xtest_update/range.txt')) + end) + + it('can append to existing file', function() + local filename = 'Xtest_update/append.txt' + local fd = io.open(filename, 'w') + fd:write('existing\n') + fd:close() + command('edit Xtest_update/new_content.txt') + command('call setline(1, "new content")') + command('update >> ' .. filename) + + eq({ 'existing', 'new content' }, fn.readfile('Xtest_update/append.txt')) + end) + + it('triggers autocmds properly', function() + command('autocmd BufWritePre * let g:write_pre = 1') + command('autocmd BufWritePost * let g:write_post = 1') + + command('edit Xtest_update/autocmd.txt') + command('call setline(1, "trigger autocmds")') + command('update') + + eq(1, eval('g:write_pre')) + eq(1, eval('g:write_post')) + end) + + it('does not write acwrite buffer when unchanged', function() + command('file remote://test') + command('setlocal buftype=acwrite') + command('let g:triggered = 0 | autocmd BufWriteCmd remote://* let g:triggered = 1') + command('update') + eq(0, eval('g:triggered')) + command('call setline(1, ["hello"])') + command('update') + eq(1, eval('g:triggered')) + end) +end) diff --git a/tests/testdata/lua_neovim_write_spec_refactor/old.lua b/tests/testdata/lua_neovim_write_spec_refactor/old.lua new file mode 100644 index 0000000..e42f46b --- /dev/null +++ b/tests/testdata/lua_neovim_write_spec_refactor/old.lua @@ -0,0 +1,385 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local eq, eval, clear, write_file, source, insert = + t.eq, n.eval, n.clear, t.write_file, n.source, n.insert +local pcall_err = t.pcall_err +local command = n.command +local feed_command = n.feed_command +local fn = n.fn +local api = n.api +local skip = t.skip +local is_os = t.is_os +local read_file = t.read_file + +local fname = 'Xtest-functional-ex_cmds-write' +local fname_bak = fname .. '~' +local fname_broken = fname_bak .. 'broken' + +describe(':write', function() + local function cleanup() + n.rmdir('Xtest_write') + os.remove('Xtest_bkc_file.txt') + os.remove('Xtest_bkc_link.txt') + os.remove('Xtest_fifo') + os.remove(fname) + os.remove(fname_bak) + os.remove(fname_broken) + end + before_each(function() + clear() + cleanup() + end) + after_each(function() + cleanup() + end) + + it('&backupcopy=auto preserves symlinks', function() + command('set backupcopy=auto') + write_file('Xtest_bkc_file.txt', 'content0') + if is_os('win') then + command('silent !mklink Xtest_bkc_link.txt Xtest_bkc_file.txt') + else + command('silent !ln -s Xtest_bkc_file.txt Xtest_bkc_link.txt') + end + if eval('v:shell_error') ~= 0 then + pending('Cannot create symlink') + end + source([[ + edit Xtest_bkc_link.txt + call setline(1, ['content1']) + write + ]]) + eq(eval("['content1']"), eval("readfile('Xtest_bkc_file.txt')")) + eq(eval("['content1']"), eval("readfile('Xtest_bkc_link.txt')")) + end) + + it('&backupcopy=no replaces symlink with new file', function() + command('set backupcopy=no') + write_file('Xtest_bkc_file.txt', 'content0') + if is_os('win') then + command('silent !mklink Xtest_bkc_link.txt Xtest_bkc_file.txt') + else + command('silent !ln -s Xtest_bkc_file.txt Xtest_bkc_link.txt') + end + if eval('v:shell_error') ~= 0 then + pending('Cannot create symlink') + end + source([[ + edit Xtest_bkc_link.txt + call setline(1, ['content1']) + write + ]]) + eq(eval("['content0']"), eval("readfile('Xtest_bkc_file.txt')")) + eq(eval("['content1']"), eval("readfile('Xtest_bkc_link.txt')")) + end) + + it('appends FIFO file', function() + -- mkfifo creates read-only .lnk files on Windows + if is_os('win') or eval("executable('mkfifo')") == 0 then + pending('N/A: missing "mkfifo" command') + end + + local text = 'some fifo text from write_spec' + assert(os.execute('mkfifo Xtest_fifo')) + insert(text) + + -- Blocks until a consumer reads the FIFO. + feed_command('write >> Xtest_fifo') + + -- Read the FIFO, this will unblock the :write above. + local fifo = assert(io.open('Xtest_fifo')) + eq(text .. '\n', fifo:read('*all')) + fifo:close() + end) + + it('++p creates missing parent directories', function() + eq(0, eval("filereadable('p_opt.txt')")) + command('write ++p p_opt.txt') + eq(1, eval("filereadable('p_opt.txt')")) + os.remove('p_opt.txt') + + eq(0, eval("filereadable('p_opt.txt')")) + command('write ++p ./p_opt.txt') + eq(1, eval("filereadable('p_opt.txt')")) + os.remove('p_opt.txt') + + eq(0, eval("filereadable('Xtest_write/write/p_opt.txt')")) + command('write ++p Xtest_write/write/p_opt.txt') + eq(1, eval("filereadable('Xtest_write/write/p_opt.txt')")) + + eq(0, eval("filereadable('Xtest_write/write2/p_opt.txt')")) + eq(0, eval("filereadable('Xtest_write/write2/p_opt2.txt')")) + eq(0, eval("filereadable('Xtest_write/write3/p_opt3.txt')")) + command('file Xtest_write/write2/p_opt.txt') + command('set modified') + command('sp Xtest_write/write2/p_opt2.txt') + command('set modified') + command('sp Xtest_write/write3/p_opt3.txt') + -- don't set p_opt3.txt modified - assert it isn't written + -- and that write3/ isn't created + command('wall ++p') + eq(1, eval("filereadable('Xtest_write/write2/p_opt.txt')")) + eq(1, eval("filereadable('Xtest_write/write2/p_opt2.txt')")) + eq(0, eval("filereadable('Xtest_write/write3/p_opt3.txt')")) + t.matches( + 'E474: Invalid argument', + pcall_err(command, 'read ++edits Xtest_write/write/p_opt.txt') + ) + + eq('Vim(write):E32: No file name', pcall_err(command, 'write ++p Xotherdir/')) + if not is_os('win') then + eq( + ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), + pcall_err(command, 'write ++p .') + ) + eq( + ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), + pcall_err(command, 'write ++p ./') + ) + end + + t.matches( + 'E474: Invalid argument', + pcall_err(command, 'write ++patate Xtest_write/garbage.txt') + ) + end) + + it('errors out correctly', function() + command('let $HOME=""') + eq(fn.fnamemodify('.', ':p:h'), fn.fnamemodify('.', ':p:h:~')) + -- Message from check_overwrite + if not is_os('win') then + eq( + ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), + pcall_err(command, 'write .') + ) + end + api.nvim_set_option_value('writeany', true, {}) + -- Message from buf_write + eq('Vim(write):E502: "." is a directory', pcall_err(command, 'write .')) + fn.mkdir(fname_bak) + api.nvim_set_option_value('backupdir', '.', {}) + api.nvim_set_option_value('backup', true, {}) + write_file(fname, 'content0') + command('edit ' .. fname) + fn.setline(1, 'TTY') + eq("Vim(write):E510: Can't make backup file (add ! to override)", pcall_err(command, 'write')) + api.nvim_set_option_value('backup', false, {}) + fn.setfperm(fname, 'r--------') + eq( + 'Vim(write):E505: "Xtest-functional-ex_cmds-write" is read-only (add ! to override)', + pcall_err(command, 'write') + ) + if is_os('win') then + eq(0, os.execute('del /q/f ' .. fname)) + eq(0, os.execute('rd /q/s ' .. fname_bak)) + else + eq(true, os.remove(fname)) + eq(true, os.remove(fname_bak)) + end + write_file(fname_bak, 'TTYX') + skip(is_os('win'), [[FIXME: pcall_err(command, 'write!') outputs 0 in Windows]]) + vim.uv.fs_symlink(fname_bak .. ('/xxxxx'):rep(20), fname) + eq("Vim(write):E166: Can't open linked file for writing", pcall_err(command, 'write!')) + end) + + it('fails converting a trailing incomplete sequence', function() + -- From https://github.com/neovim/neovim/issues/36990, an invalid UTF-8 sequence at the end of + -- the file during conversion testing can overwrite the rest of the file during the real + -- conversion. + + api.nvim_buf_set_lines(0, 0, 1, true, { 'line 1', 'line 2', 'aaabbb\235\128' }) + command('set noendofline nofixendofline') + + eq( + "Vim(write):E513: Write error, conversion failed in line 3 (make 'fenc' empty to override)", + pcall_err(command, 'write ++enc=latin1 ' .. fname) + ) + end) + + it('converts to latin1 with an invalid sequence at buffer boundary', function() + -- From https://github.com/neovim/neovim/issues/36990, an invalid UTF-8 sequence that falls + -- right at the end of the 8 KiB buffer used for encoding conversions causes subsequent data to + -- be overwritten. + + local content = string.rep('a', 1024 * 8 - 1) .. '\251' .. string.rep('b', 20) + api.nvim_buf_set_lines(0, 0, 1, true, { content }) + command('set noendofline nofixendofline fenc=latin1') + command('write ' .. fname) + + local tail = string.sub(read_file(fname) or '', -10) + eq('bbbbbbbbbb', tail) + end) + + it('converts to CP1251 with iconv', function() + api.nvim_buf_set_lines( + 0, + 0, + 1, + true, + { 'Привет, мир!', 'Это простой тест.' } + ) + command('write ++enc=cp1251 ++ff=unix ' .. fname) + + eq( + '\207\240\232\226\229\242, \236\232\240!\n' + .. '\221\242\238 \239\240\238\241\242\238\233 \242\229\241\242.\n', + read_file(fname) + ) + end) + + it('converts to GB18030 with iconv', function() + api.nvim_buf_set_lines(0, 0, 1, true, { '你好,世界!', '这是一个测试。' }) + command('write ++enc=gb18030 ++ff=unix ' .. fname) + + eq( + '\196\227\186\195\163\172\202\192\189\231\163\161\n' + .. '\213\226\202\199\210\187\184\246\178\226\202\212\161\163\n', + read_file(fname) + ) + end) + + it('converts to Shift_JIS with iconv', function() + api.nvim_buf_set_lines( + 0, + 0, + 1, + true, + { 'こんにちは、世界!', 'これはテストです。' } + ) + command('write ++enc=sjis ++ff=unix ' .. fname) + + eq( + '\130\177\130\241\130\201\130\191\130\205\129A\144\162\138E\129I\n' + .. '\130\177\130\234\130\205\131e\131X\131g\130\197\130\183\129B\n', + read_file(fname) + ) + end) + + it('fails converting an illegal sequence with iconv', function() + api.nvim_buf_set_lines(0, 0, 1, true, { 'line 1', 'aaa\128bbb' }) + + eq( + "Vim(write):E513: Write error, conversion failed (make 'fenc' empty to override)", + pcall_err(command, 'write ++enc=cp1251 ' .. fname) + ) + end) + + it('handles a multi-byte sequence crossing the buffer boundary converting with iconv', function() + local content = string.rep('a', 1024 * 8 - 1) .. 'Дbbbbb' + api.nvim_buf_set_lines(0, 0, 1, true, { content }) + -- Skip the backup so we're testing the "checking" phase also. + command('set nowritebackup') + command('write ++enc=cp1251 ++ff=unix ' .. fname) + + local expected = string.rep('a', 1024 * 8 - 1) .. '\196bbbbb\n' + eq(expected, read_file(fname)) + end) +end) + +describe(':update', function() + before_each(function() + clear() + fn.mkdir('Xtest_update', 'p') + end) + + after_each(function() + fn.delete('Xtest_update', 'rf') + end) + + it('works for a new buffer', function() + command('edit Xtest_update/foo/bar/nonexist.taz | update ++p') + eq(1, eval("filereadable('Xtest_update/foo/bar/nonexist.taz')")) + end) + + it('writes modified buffer', function() + command('edit Xtest_update/modified.txt') + command('call setline(1, "hello world")') + command('update') + eq({ 'hello world' }, fn.readfile('Xtest_update/modified.txt')) + end) + + it('does not write unmodified existing file', function() + local filename = 'Xtest_update/existing.txt' + local fd = io.open(filename, 'w') + fd:write('content') + fd:close() + local mtime_before = fn.getftime(filename) + command('edit ' .. filename) + command('update') + eq(mtime_before, fn.getftime(filename)) + end) + + it('creates parent directories with ++p', function() + command('edit Xtest_update/deep/nested/path/file.txt') + command('call setline(1, "content")') + command('update ++p') + eq(1, eval("filereadable('Xtest_update/deep/nested/path/file.txt')")) + end) + + it('fails gracefully for unnamed buffer', function() + command('enew') + command('call setline(1, "some content")') + eq('Vim(update):E32: No file name', pcall_err(command, 'update')) + end) + + it('respects readonly files', function() + local filename = 'Xtest_update/readonly.txt' + local fd = io.open(filename, 'w') + fd:write('readonly content') + fd:close() + command('edit ' .. filename) + command('set readonly') + command('call setline(1, "modified")') + eq( + "Vim(update):E45: 'readonly' option is set (add ! to override)", + pcall_err(command, 'update') + ) + + command('update!') + eq({ 'modified' }, fn.readfile('Xtest_update/readonly.txt')) + end) + + it('can write line ranges', function() + command('edit Xtest_update/range.txt') + command('call setline(1, ["line1", "line2", "line3", "line4"])') + command('2,3update!') + eq({ 'line2', 'line3' }, fn.readfile('Xtest_update/range.txt')) + end) + + it('can append to existing file', function() + local filename = 'Xtest_update/append.txt' + local fd = io.open(filename, 'w') + fd:write('existing\n') + fd:close() + command('edit Xtest_update/new_content.txt') + command('call setline(1, "new content")') + command('update >> ' .. filename) + + eq({ 'existing', 'new content' }, fn.readfile('Xtest_update/append.txt')) + end) + + it('triggers autocmds properly', function() + command('autocmd BufWritePre * let g:write_pre = 1') + command('autocmd BufWritePost * let g:write_post = 1') + + command('edit Xtest_update/autocmd.txt') + command('call setline(1, "trigger autocmds")') + command('update') + + eq(1, eval('g:write_pre')) + eq(1, eval('g:write_post')) + end) + + it('does not write acwrite buffer when unchanged', function() + command('file remote://test') + command('setlocal buftype=acwrite') + command('let g:triggered = 0 | autocmd BufWriteCmd remote://* let g:triggered = 1') + command('update') + eq(0, eval('g:triggered')) + command('call setline(1, ["hello"])') + command('update') + eq(1, eval('g:triggered')) + end) +end) diff --git a/tests/testdata/lua_neovim_zip_spec_change/expected.json b/tests/testdata/lua_neovim_zip_spec_change/expected.json new file mode 100644 index 0000000..a142c9e --- /dev/null +++ b/tests/testdata/lua_neovim_zip_spec_change/expected.json @@ -0,0 +1,3279 @@ +{ + "version": "v1", + "actions": [ + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3 + ], + "type": "variable_declaration", + "start_byte": 14366, + "end_byte": 14792 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 14196, + "end_byte": 14816 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 0 + ], + "type": "local", + "label": "local", + "start_byte": 14366, + "end_byte": 14371 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3 + ], + "type": "variable_declaration", + "start_byte": 14366, + "end_byte": 14792 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 4, + 1 + ], + "type": "arguments", + "start_byte": 14800, + "end_byte": 14816 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 4 + ], + "type": "function_call", + "start_byte": 14798, + "end_byte": 14816 + }, + "position": 1, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 0 + ], + "type": "variable_list", + "start_byte": 14372, + "end_byte": 14380 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 14372, + "end_byte": 14792 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 1 + ], + "type": "assignment_operator_literal", + "label": "=", + "start_byte": 14381, + "end_byte": 14382 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1 + ], + "type": "assignment_statement", + "start_byte": 14372, + "end_byte": 14792 + }, + "position": 1 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3 + ], + "type": "function_call", + "start_byte": 14322, + "end_byte": 14544 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2 + ], + "type": "expression_list", + "start_byte": 14383, + "end_byte": 14792 + }, + "position": 0, + "old_parent": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2 + ], + "type": "block", + "start_byte": 14196, + "end_byte": 14648 + }, + "old_position": 3, + "subtree": true, + "dest_start_byte": 14383, + "dest_end_byte": 14792 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2 + ], + "type": "return_statement", + "start_byte": 14562, + "end_byte": 14742 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2 + ], + "type": "block", + "start_byte": 14412, + "end_byte": 14742 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 0 + ], + "type": "return", + "label": "return", + "start_byte": 14562, + "end_byte": 14568 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2 + ], + "type": "return_statement", + "start_byte": 14562, + "end_byte": 14742 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0 + ], + "type": "function_call", + "start_byte": 14569, + "end_byte": 14742 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1 + ], + "type": "expression_list", + "start_byte": 14569, + "end_byte": 14742 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 0 + ], + "type": "dot_index_expression", + "start_byte": 14569, + "end_byte": 14577 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0 + ], + "type": "function_call", + "start_byte": 14569, + "end_byte": 14742 + }, + "position": 0, + "subtree": true + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 0 + ], + "type": "number", + "label": "1000", + "start_byte": 14578, + "end_byte": 14582 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1 + ], + "type": "arguments", + "start_byte": 14577, + "end_byte": 14742 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 0 + ], + "type": "function", + "label": "function", + "start_byte": 14584, + "end_byte": 14592 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 14584, + "end_byte": 14741 + }, + "position": 0 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 3 + ], + "type": "end", + "label": "end", + "start_byte": 14738, + "end_byte": 14741 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1 + ], + "type": "function_definition", + "start_byte": 14584, + "end_byte": 14741 + }, + "position": 3 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 0 + ], + "type": "return", + "label": "return", + "start_byte": 14603, + "end_byte": 14609 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0 + ], + "type": "return_statement", + "start_byte": 14603, + "end_byte": 14731 + }, + "position": 0 + }, + { + "action": "move", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 5, + 1, + 1, + 0, + 1 + ], + "type": "arguments", + "start_byte": 14609, + "end_byte": 14640 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0, + 0 + ], + "type": "function_call", + "start_byte": 14610, + "end_byte": 14724 + }, + "position": 1, + "old_parent": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 5, + 1, + 1, + 0 + ], + "type": "function_call", + "start_byte": 14580, + "end_byte": 14640 + }, + "old_position": 1, + "subtree": true, + "dest_start_byte": 14693, + "dest_end_byte": 14724 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0, + 1 + ], + "type": "comparison_operator_literal", + "label": "~=", + "start_byte": 14725, + "end_byte": 14727 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 14610, + "end_byte": 14731 + }, + "position": 1 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0, + 2 + ], + "type": "nil", + "label": "nil", + "start_byte": 14728, + "end_byte": 14731 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0 + ], + "type": "binary_expression", + "start_byte": 14610, + "end_byte": 14731 + }, + "position": 2 + }, + { + "action": "insert", + "node": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0, + 0, + 0 + ], + "type": "method_index_expression", + "start_byte": 14610, + "end_byte": 14693 + }, + "parent": { + "tree": "after", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 3, + 1, + 2, + 0, + 1, + 0, + 2, + 2, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 0, + 0 + ], + "type": "function_call", + "start_byte": 14610, + "end_byte": 14724 + }, + "position": 0, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 5, + 1, + 1, + 0, + 0 + ], + "type": "method_index_expression", + "start_byte": 14580, + "end_byte": 14609 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 5, + 1, + 1, + 1 + ], + "type": "comparison_operator_literal", + "label": "~=", + "start_byte": 14641, + "end_byte": 14643 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 5, + 1, + 1, + 2 + ], + "type": "nil", + "label": "nil", + "start_byte": 14644, + "end_byte": 14647 + } + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 4 + ], + "type": "function_call", + "start_byte": 14549, + "end_byte": 14565 + }, + "subtree": true + }, + { + "action": "delete", + "node": { + "tree": "before", + "path": [ + 16, + 1, + 1, + 2, + 13, + 1, + 1, + 2, + 5, + 1, + 0 + ], + "type": "true", + "label": "true", + "start_byte": 14574, + "end_byte": 14578 + } + } + ], + "line_alignment": [ + { + "left_line": 0, + "right_line": 0 + }, + { + "left_line": 1, + "right_line": 1 + }, + { + "left_line": 2, + "right_line": 2 + }, + { + "left_line": 3, + "right_line": 3 + }, + { + "left_line": 4, + "right_line": 4 + }, + { + "left_line": 5, + "right_line": 5 + }, + { + "left_line": 6, + "right_line": 6 + }, + { + "left_line": 7, + "right_line": 7 + }, + { + "left_line": 8, + "right_line": 8 + }, + { + "left_line": 9, + "right_line": 9 + }, + { + "left_line": 10, + "right_line": 10 + }, + { + "left_line": 11, + "right_line": 11 + }, + { + "left_line": 12, + "right_line": 12 + }, + { + "left_line": 13, + "right_line": 13 + }, + { + "left_line": 14, + "right_line": 14 + }, + { + "left_line": 15, + "right_line": 15 + }, + { + "left_line": 16, + "right_line": 16 + }, + { + "left_line": 17, + "right_line": 17 + }, + { + "left_line": 18, + "right_line": 18 + }, + { + "left_line": 19, + "right_line": 19 + }, + { + "left_line": 20, + "right_line": 20 + }, + { + "left_line": 21, + "right_line": 21 + }, + { + "left_line": 22, + "right_line": 22 + }, + { + "left_line": 23, + "right_line": 23 + }, + { + "left_line": 24, + "right_line": 24 + }, + { + "left_line": 25, + "right_line": 25 + }, + { + "left_line": 26, + "right_line": 26 + }, + { + "left_line": 27, + "right_line": 27 + }, + { + "left_line": 28, + "right_line": 28 + }, + { + "left_line": 29, + "right_line": 29 + }, + { + "left_line": 30, + "right_line": 30 + }, + { + "left_line": 31, + "right_line": 31 + }, + { + "left_line": 32, + "right_line": 32 + }, + { + "left_line": 33, + "right_line": 33 + }, + { + "left_line": 34, + "right_line": 34 + }, + { + "left_line": 35, + "right_line": 35 + }, + { + "left_line": 36, + "right_line": 36 + }, + { + "left_line": 37, + "right_line": 37 + }, + { + "left_line": 38, + "right_line": 38 + }, + { + "left_line": 39, + "right_line": 39 + }, + { + "left_line": 40, + "right_line": 40 + }, + { + "left_line": 41, + "right_line": 41 + }, + { + "left_line": 42, + "right_line": 42 + }, + { + "left_line": 43, + "right_line": 43 + }, + { + "left_line": 44, + "right_line": 44 + }, + { + "left_line": 45, + "right_line": 45 + }, + { + "left_line": 46, + "right_line": 46 + }, + { + "left_line": 47, + "right_line": 47 + }, + { + "left_line": 48, + "right_line": 48 + }, + { + "left_line": 49, + "right_line": 49 + }, + { + "left_line": 50, + "right_line": 50 + }, + { + "left_line": 51, + "right_line": 51 + }, + { + "left_line": 52, + "right_line": 52 + }, + { + "left_line": 53, + "right_line": 53 + }, + { + "left_line": 54, + "right_line": 54 + }, + { + "left_line": 55, + "right_line": 55 + }, + { + "left_line": 56, + "right_line": 56 + }, + { + "left_line": 57, + "right_line": 57 + }, + { + "left_line": 58, + "right_line": 58 + }, + { + "left_line": 59, + "right_line": 59 + }, + { + "left_line": 60, + "right_line": 60 + }, + { + "left_line": 61, + "right_line": 61 + }, + { + "left_line": 62, + "right_line": 62 + }, + { + "left_line": 63, + "right_line": 63 + }, + { + "left_line": 64, + "right_line": 64 + }, + { + "left_line": 65, + "right_line": 65 + }, + { + "left_line": 66, + "right_line": 66 + }, + { + "left_line": 67, + "right_line": 67 + }, + { + "left_line": 68, + "right_line": 68 + }, + { + "left_line": 69, + "right_line": 69 + }, + { + "left_line": 70, + "right_line": 70 + }, + { + "left_line": 71, + "right_line": 71 + }, + { + "left_line": 72, + "right_line": 72 + }, + { + "left_line": 73, + "right_line": 73 + }, + { + "left_line": 74, + "right_line": 74 + }, + { + "left_line": 75, + "right_line": 75 + }, + { + "left_line": 76, + "right_line": 76 + }, + { + "left_line": 77, + "right_line": 77 + }, + { + "left_line": 78, + "right_line": 78 + }, + { + "left_line": 79, + "right_line": 79 + }, + { + "left_line": 80, + "right_line": 80 + }, + { + "left_line": 81, + "right_line": 81 + }, + { + "left_line": 82, + "right_line": 82 + }, + { + "left_line": 83, + "right_line": 83 + }, + { + "left_line": 84, + "right_line": 84 + }, + { + "left_line": 85, + "right_line": 85 + }, + { + "left_line": 86, + "right_line": 86 + }, + { + "left_line": 87, + "right_line": 87 + }, + { + "left_line": 88, + "right_line": 88 + }, + { + "left_line": 89, + "right_line": 89 + }, + { + "left_line": 90, + "right_line": 90 + }, + { + "left_line": 91, + "right_line": 91 + }, + { + "left_line": 92, + "right_line": 92 + }, + { + "left_line": 93, + "right_line": 93 + }, + { + "left_line": 94, + "right_line": 94 + }, + { + "left_line": 95, + "right_line": 95 + }, + { + "left_line": 96, + "right_line": 96 + }, + { + "left_line": 97, + "right_line": 97 + }, + { + "left_line": 98, + "right_line": 98 + }, + { + "left_line": 99, + "right_line": 99 + }, + { + "left_line": 100, + "right_line": 100 + }, + { + "left_line": 101, + "right_line": 101 + }, + { + "left_line": 102, + "right_line": 102 + }, + { + "left_line": 103, + "right_line": 103 + }, + { + "left_line": 104, + "right_line": 104 + }, + { + "left_line": 105, + "right_line": 105 + }, + { + "left_line": 106, + "right_line": 106 + }, + { + "left_line": 107, + "right_line": 107 + }, + { + "left_line": 108, + "right_line": 108 + }, + { + "left_line": 109, + "right_line": 109 + }, + { + "left_line": 110, + "right_line": 110 + }, + { + "left_line": 111, + "right_line": 111 + }, + { + "left_line": 112, + "right_line": 112 + }, + { + "left_line": 113, + "right_line": 113 + }, + { + "left_line": 114, + "right_line": 114 + }, + { + "left_line": 115, + "right_line": 115 + }, + { + "left_line": 116, + "right_line": 116 + }, + { + "left_line": 117, + "right_line": 117 + }, + { + "left_line": 118, + "right_line": 118 + }, + { + "left_line": 119, + "right_line": 119 + }, + { + "left_line": 120, + "right_line": 120 + }, + { + "left_line": 121, + "right_line": 121 + }, + { + "left_line": 122, + "right_line": 122 + }, + { + "left_line": 123, + "right_line": 123 + }, + { + "left_line": 124, + "right_line": 124 + }, + { + "left_line": 125, + "right_line": 125 + }, + { + "left_line": 126, + "right_line": 126 + }, + { + "left_line": 127, + "right_line": 127 + }, + { + "left_line": 128, + "right_line": 128 + }, + { + "left_line": 129, + "right_line": 129 + }, + { + "left_line": 130, + "right_line": 130 + }, + { + "left_line": 131, + "right_line": 131 + }, + { + "left_line": 132, + "right_line": 132 + }, + { + "left_line": 133, + "right_line": 133 + }, + { + "left_line": 134, + "right_line": 134 + }, + { + "left_line": 135, + "right_line": 135 + }, + { + "left_line": 136, + "right_line": 136 + }, + { + "left_line": 137, + "right_line": 137 + }, + { + "left_line": 138, + "right_line": 138 + }, + { + "left_line": 139, + "right_line": 139 + }, + { + "left_line": 140, + "right_line": 140 + }, + { + "left_line": 141, + "right_line": 141 + }, + { + "left_line": 142, + "right_line": 142 + }, + { + "left_line": 143, + "right_line": 143 + }, + { + "left_line": 144, + "right_line": 144 + }, + { + "left_line": 145, + "right_line": 145 + }, + { + "left_line": 146, + "right_line": 146 + }, + { + "left_line": 147, + "right_line": 147 + }, + { + "left_line": 148, + "right_line": 148 + }, + { + "left_line": 149, + "right_line": 149 + }, + { + "left_line": 150, + "right_line": 150 + }, + { + "left_line": 151, + "right_line": 151 + }, + { + "left_line": 152, + "right_line": 152 + }, + { + "left_line": 153, + "right_line": 153 + }, + { + "left_line": 154, + "right_line": 154 + }, + { + "left_line": 155, + "right_line": 155 + }, + { + "left_line": 156, + "right_line": 156 + }, + { + "left_line": 157, + "right_line": 157 + }, + { + "left_line": 158, + "right_line": 158 + }, + { + "left_line": 159, + "right_line": 159 + }, + { + "left_line": 160, + "right_line": 160 + }, + { + "left_line": 161, + "right_line": 161 + }, + { + "left_line": 162, + "right_line": 162 + }, + { + "left_line": 163, + "right_line": 163 + }, + { + "left_line": 164, + "right_line": 164 + }, + { + "left_line": 165, + "right_line": 165 + }, + { + "left_line": 166, + "right_line": 166 + }, + { + "left_line": 167, + "right_line": 167 + }, + { + "left_line": 168, + "right_line": 168 + }, + { + "left_line": 169, + "right_line": 169 + }, + { + "left_line": 170, + "right_line": 170 + }, + { + "left_line": 171, + "right_line": 171 + }, + { + "left_line": 172, + "right_line": 172 + }, + { + "left_line": 173, + "right_line": 173 + }, + { + "left_line": 174, + "right_line": 174 + }, + { + "left_line": 175, + "right_line": 175 + }, + { + "left_line": 176, + "right_line": 176 + }, + { + "left_line": 177, + "right_line": 177 + }, + { + "left_line": 178, + "right_line": 178 + }, + { + "left_line": 179, + "right_line": 179 + }, + { + "left_line": 180, + "right_line": 180 + }, + { + "left_line": 181, + "right_line": 181 + }, + { + "left_line": 182, + "right_line": 182 + }, + { + "left_line": 183, + "right_line": 183 + }, + { + "left_line": 184, + "right_line": 184 + }, + { + "left_line": 185, + "right_line": 185 + }, + { + "left_line": 186, + "right_line": 186 + }, + { + "left_line": 187, + "right_line": 187 + }, + { + "left_line": 188, + "right_line": 188 + }, + { + "left_line": 189, + "right_line": 189 + }, + { + "left_line": 190, + "right_line": 190 + }, + { + "left_line": 191, + "right_line": 191 + }, + { + "left_line": 192, + "right_line": 192 + }, + { + "left_line": 193, + "right_line": 193 + }, + { + "left_line": 194, + "right_line": 194 + }, + { + "left_line": 195, + "right_line": 195 + }, + { + "left_line": 196, + "right_line": 196 + }, + { + "left_line": 197, + "right_line": 197 + }, + { + "left_line": 198, + "right_line": 198 + }, + { + "left_line": 199, + "right_line": 199 + }, + { + "left_line": 200, + "right_line": 200 + }, + { + "left_line": 201, + "right_line": 201 + }, + { + "left_line": 202, + "right_line": 202 + }, + { + "left_line": 203, + "right_line": 203 + }, + { + "left_line": 204, + "right_line": 204 + }, + { + "left_line": 205, + "right_line": 205 + }, + { + "left_line": 206, + "right_line": 206 + }, + { + "left_line": 207, + "right_line": 207 + }, + { + "left_line": 208, + "right_line": 208 + }, + { + "left_line": 209, + "right_line": 209 + }, + { + "left_line": 210, + "right_line": 210 + }, + { + "left_line": 211, + "right_line": 211 + }, + { + "left_line": 212, + "right_line": 212 + }, + { + "left_line": 213, + "right_line": 213 + }, + { + "left_line": 214, + "right_line": 214 + }, + { + "left_line": 215, + "right_line": 215 + }, + { + "left_line": 216, + "right_line": 216 + }, + { + "left_line": 217, + "right_line": 217 + }, + { + "left_line": 218, + "right_line": 218 + }, + { + "left_line": 219, + "right_line": 219 + }, + { + "left_line": 220, + "right_line": 220 + }, + { + "left_line": 221, + "right_line": 221 + }, + { + "left_line": 222, + "right_line": 222 + }, + { + "left_line": 223, + "right_line": 223 + }, + { + "left_line": 224, + "right_line": 224 + }, + { + "left_line": 225, + "right_line": 225 + }, + { + "left_line": 226, + "right_line": 226 + }, + { + "left_line": 227, + "right_line": 227 + }, + { + "left_line": 228, + "right_line": 228 + }, + { + "left_line": 229, + "right_line": 229 + }, + { + "left_line": 230, + "right_line": 230 + }, + { + "left_line": 231, + "right_line": 231 + }, + { + "left_line": 232, + "right_line": 232 + }, + { + "left_line": 233, + "right_line": 233 + }, + { + "left_line": 234, + "right_line": 234 + }, + { + "left_line": 235, + "right_line": 235 + }, + { + "left_line": 236, + "right_line": 236 + }, + { + "left_line": 237, + "right_line": 237 + }, + { + "left_line": 238, + "right_line": 238 + }, + { + "left_line": 239, + "right_line": 239 + }, + { + "left_line": 240, + "right_line": 240 + }, + { + "left_line": 241, + "right_line": 241 + }, + { + "left_line": 242, + "right_line": 242 + }, + { + "left_line": 243, + "right_line": 243 + }, + { + "left_line": 244, + "right_line": 244 + }, + { + "left_line": 245, + "right_line": 245 + }, + { + "left_line": 246, + "right_line": 246 + }, + { + "left_line": 247, + "right_line": 247 + }, + { + "left_line": 248, + "right_line": 248 + }, + { + "left_line": 249, + "right_line": 249 + }, + { + "left_line": 250, + "right_line": 250 + }, + { + "left_line": 251, + "right_line": 251 + }, + { + "left_line": 252, + "right_line": 252 + }, + { + "left_line": 253, + "right_line": 253 + }, + { + "left_line": 254, + "right_line": 254 + }, + { + "left_line": 255, + "right_line": 255 + }, + { + "left_line": 256, + "right_line": 256 + }, + { + "left_line": 257, + "right_line": 257 + }, + { + "left_line": 258, + "right_line": 258 + }, + { + "left_line": 259, + "right_line": 259 + }, + { + "left_line": 260, + "right_line": 260 + }, + { + "left_line": 261, + "right_line": 261 + }, + { + "left_line": 262, + "right_line": 262 + }, + { + "left_line": 263, + "right_line": 263 + }, + { + "left_line": 264, + "right_line": 264 + }, + { + "left_line": 265, + "right_line": 265 + }, + { + "left_line": 266, + "right_line": 266 + }, + { + "left_line": 267, + "right_line": 267 + }, + { + "left_line": 268, + "right_line": 268 + }, + { + "left_line": 269, + "right_line": 269 + }, + { + "left_line": 270, + "right_line": 270 + }, + { + "left_line": 271, + "right_line": 271 + }, + { + "left_line": 272, + "right_line": 272 + }, + { + "left_line": 273, + "right_line": 273 + }, + { + "left_line": 274, + "right_line": 274 + }, + { + "left_line": 275, + "right_line": 275 + }, + { + "left_line": 276, + "right_line": 276 + }, + { + "left_line": 277, + "right_line": 277 + }, + { + "left_line": 278, + "right_line": 278 + }, + { + "left_line": 279, + "right_line": 279 + }, + { + "left_line": 280, + "right_line": 280 + }, + { + "left_line": 281, + "right_line": 281 + }, + { + "left_line": 282, + "right_line": 282 + }, + { + "left_line": 283, + "right_line": 283 + }, + { + "left_line": 284, + "right_line": 284 + }, + { + "left_line": 285, + "right_line": 285 + }, + { + "left_line": 286, + "right_line": 286 + }, + { + "left_line": 287, + "right_line": 287 + }, + { + "left_line": 288, + "right_line": 288 + }, + { + "left_line": 289, + "right_line": 289 + }, + { + "left_line": 290, + "right_line": 290 + }, + { + "left_line": 291, + "right_line": 291 + }, + { + "left_line": 292, + "right_line": 292 + }, + { + "left_line": 293, + "right_line": 293 + }, + { + "left_line": 294, + "right_line": 294 + }, + { + "left_line": 295, + "right_line": 295 + }, + { + "left_line": 296, + "right_line": 296 + }, + { + "left_line": 297, + "right_line": 297 + }, + { + "left_line": 298, + "right_line": 298 + }, + { + "left_line": 299, + "right_line": 299 + }, + { + "left_line": 300, + "right_line": 300 + }, + { + "left_line": 301, + "right_line": 301 + }, + { + "left_line": 302, + "right_line": 302 + }, + { + "left_line": 303, + "right_line": 303 + }, + { + "left_line": 304, + "right_line": 304 + }, + { + "left_line": 305, + "right_line": 305 + }, + { + "left_line": 306, + "right_line": 306 + }, + { + "left_line": 307, + "right_line": 307 + }, + { + "left_line": 308, + "right_line": 308 + }, + { + "left_line": 309, + "right_line": 309 + }, + { + "left_line": 310, + "right_line": 310 + }, + { + "left_line": 311, + "right_line": 311 + }, + { + "left_line": 312, + "right_line": 312 + }, + { + "left_line": 313, + "right_line": 313 + }, + { + "left_line": 314, + "right_line": 314 + }, + { + "left_line": 315, + "right_line": 315 + }, + { + "left_line": 316, + "right_line": 316 + }, + { + "left_line": 317, + "right_line": 317 + }, + { + "left_line": 318, + "right_line": 318 + }, + { + "left_line": 319, + "right_line": 319 + }, + { + "left_line": 320, + "right_line": 320 + }, + { + "left_line": 321, + "right_line": 321 + }, + { + "left_line": 322, + "right_line": 322 + }, + { + "left_line": 323, + "right_line": 323 + }, + { + "left_line": 324, + "right_line": 324 + }, + { + "left_line": 325, + "right_line": 325 + }, + { + "left_line": 326, + "right_line": 326 + }, + { + "left_line": 327, + "right_line": 327 + }, + { + "left_line": 328, + "right_line": 328 + }, + { + "left_line": 329, + "right_line": 329 + }, + { + "left_line": 330, + "right_line": 330 + }, + { + "left_line": 331, + "right_line": 331 + }, + { + "left_line": 332, + "right_line": 332 + }, + { + "left_line": 333, + "right_line": 333 + }, + { + "left_line": 334, + "right_line": 334 + }, + { + "left_line": 335, + "right_line": 335 + }, + { + "left_line": 336, + "right_line": 336 + }, + { + "left_line": 337, + "right_line": 337 + }, + { + "left_line": 338, + "right_line": 338 + }, + { + "left_line": 339, + "right_line": 339 + }, + { + "left_line": 340, + "right_line": 340 + }, + { + "left_line": 341, + "right_line": 341 + }, + { + "left_line": 342, + "right_line": 342 + }, + { + "left_line": 343, + "right_line": 343 + }, + { + "left_line": 344, + "right_line": 344 + }, + { + "left_line": 345, + "right_line": 345 + }, + { + "left_line": 346, + "right_line": 346 + }, + { + "left_line": 347, + "right_line": 347 + }, + { + "left_line": 348, + "right_line": 348 + }, + { + "left_line": 349, + "right_line": 349 + }, + { + "left_line": 350, + "right_line": 350 + }, + { + "left_line": 351, + "right_line": 351 + }, + { + "left_line": 352, + "right_line": 352 + }, + { + "left_line": 353, + "right_line": 353 + }, + { + "left_line": 354, + "right_line": 354 + }, + { + "left_line": 355, + "right_line": 355 + }, + { + "left_line": 356, + "right_line": 356 + }, + { + "left_line": 357, + "right_line": 357 + }, + { + "left_line": 358, + "right_line": 358 + }, + { + "left_line": 359, + "right_line": 359 + }, + { + "left_line": 360, + "right_line": 360 + }, + { + "left_line": 361, + "right_line": 361 + }, + { + "left_line": 362, + "right_line": 362 + }, + { + "left_line": 363, + "right_line": 363 + }, + { + "left_line": 364, + "right_line": 364 + }, + { + "left_line": 365, + "right_line": 365 + }, + { + "left_line": 366, + "right_line": 366 + }, + { + "left_line": 367, + "right_line": 367 + }, + { + "left_line": 368, + "right_line": 368 + }, + { + "left_line": 369, + "right_line": 369 + }, + { + "left_line": 370, + "right_line": 370 + }, + { + "left_line": 371, + "right_line": 371 + }, + { + "left_line": 372, + "right_line": 372 + }, + { + "left_line": 373, + "right_line": 373 + }, + { + "left_line": 374, + "right_line": 374 + }, + { + "left_line": 375, + "right_line": 375 + }, + { + "left_line": 376, + "right_line": 376 + }, + { + "left_line": 377, + "right_line": 377 + }, + { + "left_line": 378, + "right_line": 378 + }, + { + "left_line": 379, + "right_line": 379 + }, + { + "left_line": 380, + "right_line": 380 + }, + { + "left_line": 381, + "right_line": 381 + }, + { + "left_line": 382, + "right_line": 382 + }, + { + "left_line": 383, + "right_line": 383 + }, + { + "left_line": 384, + "right_line": 384 + }, + { + "left_line": 385, + "right_line": 385 + }, + { + "left_line": 386, + "right_line": 386 + }, + { + "left_line": 387, + "right_line": 387 + }, + { + "left_line": 388, + "right_line": 388 + }, + { + "left_line": 389, + "right_line": 389 + }, + { + "left_line": 390, + "right_line": 390 + }, + { + "left_line": 391, + "right_line": 391 + }, + { + "left_line": 392, + "right_line": 392 + }, + { + "left_line": 393, + "right_line": 393 + }, + { + "left_line": 394, + "right_line": 394 + }, + { + "left_line": 395, + "right_line": 395 + }, + { + "left_line": 396, + "right_line": 396 + }, + { + "left_line": 397, + "right_line": 397 + }, + { + "left_line": 398, + "right_line": 398 + }, + { + "left_line": 399, + "right_line": 399 + }, + { + "left_line": 400, + "right_line": 400 + }, + { + "left_line": 401, + "right_line": 401 + }, + { + "left_line": 402, + "right_line": 402 + }, + { + "left_line": 403, + "right_line": 403 + }, + { + "left_line": 404, + "right_line": 404 + }, + { + "left_line": 405, + "right_line": 405 + }, + { + "left_line": 406, + "right_line": 406 + }, + { + "left_line": 407, + "right_line": 407 + }, + { + "left_line": 408, + "right_line": 408 + }, + { + "left_line": 409, + "right_line": 409 + }, + { + "left_line": 410, + "right_line": 410 + }, + { + "left_line": 411, + "right_line": 411 + }, + { + "left_line": 412, + "right_line": 412 + }, + { + "left_line": 413, + "right_line": 413 + }, + { + "left_line": 414, + "right_line": 414 + }, + { + "left_line": 415, + "right_line": 415 + }, + { + "left_line": 416, + "right_line": 416 + }, + { + "left_line": 417, + "right_line": 417 + }, + { + "left_line": 418, + "right_line": 418 + }, + { + "left_line": 419, + "right_line": 419 + }, + { + "left_line": 420, + "right_line": 420 + }, + { + "left_line": 421, + "right_line": 421 + }, + { + "left_line": 422, + "right_line": 422 + }, + { + "left_line": 423, + "right_line": 423 + }, + { + "left_line": 424, + "right_line": 424 + }, + { + "left_line": 425, + "right_line": 425 + }, + { + "left_line": 426, + "right_line": 426 + }, + { + "left_line": 427, + "right_line": 427 + }, + { + "left_line": 428, + "right_line": 428 + }, + { + "left_line": 429, + "right_line": 429 + }, + { + "left_line": 430, + "right_line": 430 + }, + { + "left_line": 431, + "right_line": 431 + }, + { + "left_line": 432, + "right_line": 432 + }, + { + "left_line": 433, + "right_line": 433 + }, + { + "left_line": 434, + "right_line": 434 + }, + { + "left_line": 435, + "right_line": 435 + }, + { + "left_line": 436, + "right_line": 436 + }, + { + "left_line": 437, + "right_line": 437 + }, + { + "left_line": 438, + "right_line": 438 + }, + { + "left_line": 439, + "right_line": 439 + }, + { + "left_line": 440, + "right_line": 440 + }, + { + "left_line": 441, + "right_line": 441 + }, + { + "left_line": 442, + "right_line": 442 + }, + { + "left_line": 443, + "right_line": 443 + }, + { + "left_line": 444, + "right_line": 444 + }, + { + "left_line": 445, + "right_line": 445 + }, + { + "left_line": 446, + "right_line": 446 + }, + { + "left_line": 447, + "right_line": 447 + }, + { + "left_line": 448, + "right_line": 448 + }, + { + "left_line": -1, + "right_line": 449 + }, + { + "left_line": -1, + "right_line": 450 + }, + { + "left_line": -1, + "right_line": 451 + }, + { + "left_line": -1, + "right_line": 452 + }, + { + "left_line": -1, + "right_line": 453 + }, + { + "left_line": 449, + "right_line": 454 + }, + { + "left_line": 450, + "right_line": -1 + }, + { + "left_line": 451, + "right_line": 455 + }, + { + "left_line": 452, + "right_line": 456 + }, + { + "left_line": 453, + "right_line": 457 + }, + { + "left_line": 454, + "right_line": 458 + }, + { + "left_line": 455, + "right_line": 459 + }, + { + "left_line": 456, + "right_line": 460 + }, + { + "left_line": 457, + "right_line": 461 + }, + { + "left_line": 458, + "right_line": 462 + }, + { + "left_line": 459, + "right_line": 463 + }, + { + "left_line": 460, + "right_line": 464 + }, + { + "left_line": 461, + "right_line": 465 + }, + { + "left_line": 462, + "right_line": 466 + }, + { + "left_line": 463, + "right_line": 467 + }, + { + "left_line": 464, + "right_line": 468 + }, + { + "left_line": 465, + "right_line": 469 + }, + { + "left_line": 466, + "right_line": 470 + }, + { + "left_line": 467, + "right_line": 471 + }, + { + "left_line": 468, + "right_line": 472 + }, + { + "left_line": 469, + "right_line": 473 + }, + { + "left_line": 470, + "right_line": 474 + }, + { + "left_line": 471, + "right_line": 475 + }, + { + "left_line": 472, + "right_line": 476 + }, + { + "left_line": 473, + "right_line": 477 + }, + { + "left_line": 474, + "right_line": 478 + }, + { + "left_line": 475, + "right_line": 479 + }, + { + "left_line": 476, + "right_line": 480 + }, + { + "left_line": 477, + "right_line": 481 + }, + { + "left_line": 478, + "right_line": 482 + }, + { + "left_line": 479, + "right_line": 483 + }, + { + "left_line": 480, + "right_line": 484 + }, + { + "left_line": 481, + "right_line": 485 + }, + { + "left_line": 482, + "right_line": 486 + }, + { + "left_line": 483, + "right_line": 487 + }, + { + "left_line": 484, + "right_line": 488 + }, + { + "left_line": 485, + "right_line": 489 + }, + { + "left_line": 486, + "right_line": 490 + }, + { + "left_line": 487, + "right_line": 491 + }, + { + "left_line": 488, + "right_line": 492 + }, + { + "left_line": 489, + "right_line": 493 + }, + { + "left_line": 490, + "right_line": 494 + }, + { + "left_line": 491, + "right_line": 495 + }, + { + "left_line": 492, + "right_line": 496 + }, + { + "left_line": 493, + "right_line": 497 + }, + { + "left_line": 494, + "right_line": 498 + }, + { + "left_line": 495, + "right_line": 499 + }, + { + "left_line": 496, + "right_line": 500 + }, + { + "left_line": 497, + "right_line": 501 + }, + { + "left_line": 498, + "right_line": 502 + }, + { + "left_line": 499, + "right_line": 503 + }, + { + "left_line": 500, + "right_line": 504 + }, + { + "left_line": 501, + "right_line": 505 + }, + { + "left_line": 502, + "right_line": 506 + }, + { + "left_line": 503, + "right_line": 507 + }, + { + "left_line": 504, + "right_line": 508 + }, + { + "left_line": 505, + "right_line": 509 + }, + { + "left_line": 506, + "right_line": 510 + }, + { + "left_line": 507, + "right_line": 511 + }, + { + "left_line": 508, + "right_line": 512 + }, + { + "left_line": 509, + "right_line": 513 + }, + { + "left_line": 510, + "right_line": 514 + }, + { + "left_line": 511, + "right_line": 515 + }, + { + "left_line": 512, + "right_line": 516 + }, + { + "left_line": 513, + "right_line": 517 + }, + { + "left_line": 514, + "right_line": 518 + }, + { + "left_line": 515, + "right_line": 519 + }, + { + "left_line": 516, + "right_line": 520 + }, + { + "left_line": 517, + "right_line": 521 + }, + { + "left_line": 518, + "right_line": 522 + }, + { + "left_line": 519, + "right_line": 523 + }, + { + "left_line": 520, + "right_line": 524 + }, + { + "left_line": 521, + "right_line": 525 + }, + { + "left_line": 522, + "right_line": 526 + }, + { + "left_line": 523, + "right_line": 527 + }, + { + "left_line": 524, + "right_line": 528 + }, + { + "left_line": 525, + "right_line": 529 + }, + { + "left_line": 526, + "right_line": 530 + }, + { + "left_line": 527, + "right_line": 531 + } + ] +} \ No newline at end of file diff --git a/tests/testdata/lua_neovim_zip_spec_change/metadata.json b/tests/testdata/lua_neovim_zip_spec_change/metadata.json new file mode 100644 index 0000000..9b08a79 --- /dev/null +++ b/tests/testdata/lua_neovim_zip_spec_change/metadata.json @@ -0,0 +1,16 @@ +{ + "repo": "https://github.com/neovim/neovim", + "parent": "f6de99d028c27337cdbac2e1d1c1b954e9badc48", + "commit": "9c5afe66", + "message": "test: unreliable "nvim.zip … incorrect password" #41159", + "author": "Justin M. Keyes", + "date": "2026-08-04T14:00:13-04:00", + "language": "lua", + "files_changed": 1, + "insertions": FAILED +…/plugin/zip_spec.lua:453: +8, + "deletions": …/plugin/zip_spec.lua +in +4 +} diff --git a/tests/testdata/lua_neovim_zip_spec_change/new.lua b/tests/testdata/lua_neovim_zip_spec_change/new.lua new file mode 100644 index 0000000..a8592b2 --- /dev/null +++ b/tests/testdata/lua_neovim_zip_spec_change/new.lua @@ -0,0 +1,531 @@ +local n = require('test.functional.testnvim')() +local t = require('test.testutil') + +local describe, it, before_each, after_each = t.describe, t.it, t.before_each, t.after_each +local api = n.api +local eq = t.eq +local exec_capture = n.exec_capture +local exec_lua = n.exec_lua +local feed = n.feed +local fn = n.fn +local poke_eventloop = n.poke_eventloop + +local fixtures = vim.fs.joinpath(t.paths.test_source_path, 'test/functional/fixtures/zip') +local old_samples = vim.fs.joinpath(t.paths.test_source_path, 'test/old/testdir/samples') + +local function lines() + return api.nvim_buf_get_lines(0, 0, -1, false) +end + +local function edit(path) + api.nvim_cmd({ cmd = 'edit', args = { path }, magic = { file = false, bar = false } }, {}) +end + +local function clear_zip() + n.clear({ args = { '--clean' } }) +end + +local function line_of(text) + for i, line in ipairs(lines()) do + if line == text then + return i + end + end + error(('missing line %q in %s'):format(text, vim.inspect(lines()))) +end + +describe('nvim.zip', function() + local root + + --- Copy an archive into the test directory and return its path. + local function stage(source_dir, source, as) + local target = vim.fs.joinpath(root, as or source) + assert(vim.uv.fs_copyfile(vim.fs.joinpath(source_dir, source), target)) + return target + end + + --- Stage the browsing fixture, restart, and open it. + local function browse(as) + local archive = stage(fixtures, 'browser.zip', as) + clear_zip() + edit(archive) + return archive + end + + before_each(function() + t.skip(vim.fn.executable('unzip') == 0, 'unzip not available') + root = vim.fs.normalize(t.tmpname(false) .. ' space%#') + t.mkdir(root) + end) + + after_each(function() + n.rmdir(root) + end) + + describe('activation', function() + it('uses zip.lua by default', function() + browse() + + eq('folder/', lines()[1]) + eq(true, exec_lua('return vim.g.loaded_nvim_zip_plugin == true')) + eq(false, exec_lua('return vim.g.loaded_zipPlugin ~= nil')) + end) + + it('defers to zipPlugin.vim loaded before startup plugins', function() + local archive = stage(old_samples, 'test.zip', 'legacy.zip') + n.clear({ args = { '--clean', '--cmd', 'packadd old-zip' } }) + + edit(archive) + + eq(true, lines()[1]:find('" zip.vim version', 1, true) ~= nil) + eq(0, fn.exists('#nvim.zip')) + eq(false, exec_lua('return vim.g.loaded_nvim_zip_plugin ~= nil')) + end) + + it('yields to zipPlugin.vim loaded after startup', function() + local archive = stage(old_samples, 'test.zip', 'legacy.zip') + clear_zip() + exec_lua([[vim.cmd.packadd('old-zip')]]) + + edit(archive) + + eq(true, lines()[1]:find('" zip.vim version', 1, true) ~= nil) + eq(1, fn.exists('#nvim.zip')) + eq(true, exec_lua('return vim.g.loaded_nvim_zip_plugin == true')) + end) + + it('can be disabled', function() + local archive = stage(fixtures, 'browser.zip') + n.clear({ + args = { '--clean', '--cmd', 'let g:loaded_nvim_zip_plugin = 1' }, + }) + + edit(archive) + + eq(0, fn.exists('#nvim.zip')) + eq('', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('can source zip.lua repeatedly', function() + clear_zip() + + -- Re-sourcing must reuse the augroup rather than stack duplicate autocmds. + eq( + 2, + exec_lua(function() + vim.cmd.runtime('plugin/zip.lua') + vim.cmd.runtime('plugin/zip.lua') + local ids = {} + for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ group = 'nvim.zip' })) do + ids[autocmd.id] = true + end + return vim.tbl_count(ids) + end) + ) + end) + + it('reports an unavailable backend without claiming the buffer', function() + local archive = stage(fixtures, 'browser.zip') + clear_zip() + exec_lua([[vim.env.PATH = '']]) + + edit(archive) + poke_eventloop() + + eq(true, exec_capture('messages'):find('unzip executable not found', 1, true) ~= nil) + eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) + end) + end) + + describe('browsing', function() + it('opens zip-compatible file types', function() + browse('browser.jar') + + eq('folder/', lines()[1]) + eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('browses directories and opens entries', function() + browse() + + eq({ + 'folder/', + 'inner.zip', + '../escape.txt', + '/absolute.txt', + 'crlf.txt', + 'noeol.txt', + 'names/', + }, lines()) + eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) + eq(true, exec_capture('syntax list zipDirectory'):find('zipDirectory', 1, true) ~= nil) + + feed('') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + + feed('') + poke_eventloop() + eq({ 'file.txt' }, lines()) + + feed('-') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + eq('nested/', api.nvim_get_current_line()) + + api.nvim_win_set_cursor(0, { 2, 0 }) + feed('') + poke_eventloop() + eq({ 'root text' }, lines()) + eq('nowrite', api.nvim_get_option_value('buftype', { buf = 0 })) + eq(true, api.nvim_get_option_value('readonly', { buf = 0 })) + eq(false, api.nvim_get_option_value('modifiable', { buf = 0 })) + eq(false, api.nvim_get_option_value('swapfile', { buf = 0 })) + end) + + it('opens the containing directory from the archive root', function() + browse() + feed('-') + poke_eventloop() + + eq(root, vim.fs.normalize(api.nvim_buf_get_name(0))) + eq('browser.zip', api.nvim_get_current_line()) + eq('directory', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('opens entries at quickfix locations', function() + local archive = stage(fixtures, 'browser.zip') + clear_zip() + local uri = ('zip://%s/crlf.txt'):format(archive) + fn.setqflist({}, 'r', { items = { { filename = uri, lnum = 2, col = 1 } } }) + + api.nvim_cmd({ cmd = 'cfirst' }, {}) + + eq(uri, api.nvim_buf_get_name(0)) + eq({ 2, 0 }, api.nvim_win_get_cursor(0)) + eq('text', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('does not reinterpret an entry ending in .zip as an archive', function() + browse() + api.nvim_win_set_cursor(0, { line_of('inner.zip'), 0 }) + feed('') + poke_eventloop() + + eq({ 'nested payload' }, lines()) + eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) + end) + + it('preserves normal file reading details for entries', function() + local archive = stage(fixtures, 'browser.zip') + clear_zip() + + edit(('zip://%s/crlf.txt'):format(archive)) + eq({ 'one', 'two' }, lines()) + eq('dos', api.nvim_get_option_value('fileformat', { buf = 0 })) + eq(true, api.nvim_get_option_value('endofline', { buf = 0 })) + + edit(('zip://%s/noeol.txt'):format(archive)) + eq({ 'no final newline' }, lines()) + eq(false, api.nvim_get_option_value('endofline', { buf = 0 })) + end) + + it('keeps suspicious entry paths visible and readable', function() + browse() + api.nvim_win_set_cursor(0, { line_of('../escape.txt'), 0 }) + feed('') + poke_eventloop() + + eq({ 'escape payload' }, lines()) + end) + + it('opens non-zip files normally', function() + local archive = vim.fs.joinpath(root, 'plain.zip') + t.write_file(archive, 'plain text', true) + clear_zip() + + edit(archive) + + eq({ 'plain text' }, lines()) + eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) + end) + + it('lists a valid empty archive as empty', function() + local archive = vim.fs.joinpath(root, 'empty.zip') + local file = assert(io.open(archive, 'wb')) + -- An end-of-central-directory record with no entries: a valid, empty archive. + file:write('PK\5\6' .. string.rep('\0', 18)) + file:close() + clear_zip() + + edit(archive) + + eq({ '' }, lines()) + eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) + eq(true, exec_lua('return vim.b.nvim_zip ~= nil')) + end) + + it('keeps the current level when reload fails', function() + local archive = browse() + feed('') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + + assert(os.remove(archive)) + feed('R') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + + stage(fixtures, 'browser.zip') + feed('R') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + end) + + it('integrates Java sources with zip.lua and zipPlugin.vim', function() + local archive = stage(fixtures, 'browser.zip', 'source.jar') + + for _, legacy in ipairs({ false, true }) do + if legacy then + n.clear({ args = { '--clean', '--cmd', 'packadd old-zip' } }) + else + clear_zip() + end + exec_lua('vim.g.ftplugin_java_source_path = ...', archive) + api.nvim_buf_set_name(0, vim.fs.joinpath(root, 'Test.java')) + api.nvim_buf_set_lines(0, 0, -1, false, { 'folder.root' }) + api.nvim_cmd({ cmd = 'setfiletype', args = { 'java' } }, {}) + api.nvim_cmd({ cmd = 'runtime', args = { 'ftplugin/java.vim' } }, {}) + + feed('gf') + poke_eventloop() + + local uri = legacy and ('zipfile://%s::folder/root.java'):format(archive) + or ('zip://%s/folder/root.java'):format(archive) + eq(uri, api.nvim_buf_get_name(0)) + eq({ 'class root {}' }, lines()) + eq('java', api.nvim_get_option_value('filetype', { buf = 0 })) + end + end) + end) + + -- Info-ZIP expands globs itself and reads leading dashes as options, so archive + -- and entry paths must always reach it as literal text. + describe('backend arguments', function() + it('reads entry selectors literally', function() + t.skip(t.is_os('win'), 'N/A: archive contains backslashes in entry paths') + local archive = stage(old_samples, 'testa.zip', 'special.zip') + clear_zip() + + local cases = { + { 'zipglob/a[a].txt', 'a test file with []' }, + { 'zipglob/a*.txt', 'a test file with a*' }, + { 'zipglob/a?.txt', 'a test file with a?' }, + { [[zipglob/a\.txt]], [[a test file with a\]] }, + { [[zipglob/a\\.txt]], [[a test file with a double \]] }, + } + for _, case in ipairs(cases) do + edit(('zip://%s/%s'):format(archive, case[1])) + eq({ case[2] }, lines()) + end + end) + + it('runs the backend when the cwd is its directory', function() + t.skip(t.is_os('win'), 'N/A: Windows searches the cwd before $PATH') + local archive = stage(fixtures, 'browser.zip') + local bin = vim.fs.joinpath(assert(vim.uv.fs_realpath(root)), 'bin') + t.mkdir(bin) + assert(vim.uv.fs_symlink(vim.fn.exepath('unzip'), vim.fs.joinpath(bin, 'unzip'))) + n.clear({ args = { '--clean' }, env = { PATH = bin .. ':' .. os.getenv('PATH') } }) + + api.nvim_set_current_dir(bin) + edit(archive) + eq('folder/', lines()[1]) + end) + + it('keeps a leading dash in a path from becoming a backend option', function() + local archive = stage(old_samples, 'poc.zip') + clear_zip() + + edit(archive) + eq({ '-d/', 'pwned' }, lines()) + + edit(('zip://%s/-d/tmp'):format(archive)) + eq({ '' }, lines()) + end) + + it('treats archive glob characters literally', function() + t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') + local archive = stage(fixtures, 'browser.zip', 'archive::[*?].zip') + -- Would be matched instead of the archive above if the name were globbed. + stage(old_samples, 'test.zip', 'archivex.zip') + clear_zip() + + edit(archive) + + eq('folder/', lines()[1]) + api.nvim_win_set_cursor(0, { line_of('inner.zip'), 0 }) + feed('') + poke_eventloop() + eq({ 'nested payload' }, lines()) + + local uri = ('zip://%s/crlf.txt'):format(archive) + edit(uri) + eq(uri, api.nvim_buf_get_name(0)) + eq({ 'one', 'two' }, lines()) + end) + + it('lists and opens entries with difficult names', function() + t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') + local archive = browse() + + -- "star*" and "-dash" are the glob and option cases; the rest are shell + -- metacharacters that must survive an argv-based backend untouched. + local names = { + 'space name.txt', + 'two spaces.txt', + ' leading.txt', + 'trailing .txt', + [[back\slash.txt]], + [[single'quote.txt]], + 'double"quote.txt', + 'dollar$name.txt', + 'backtick`name.txt', + 'semi;name.txt', + 'pipe|name.txt', + 'amp&name.txt', + 'paren(name).txt', + 'bang!name.txt', + 'hash#name.txt', + 'percent%name.txt', + 'star*.txt', + 'question?.txt', + '[bracket].txt', + '-dash.txt', + } + + api.nvim_win_set_cursor(0, { line_of('names/'), 0 }) + feed('') + poke_eventloop() + eq(names, lines()) + + api.nvim_win_set_cursor(0, { line_of('star*.txt'), 0 }) + feed('') + poke_eventloop() + eq(('zip://%s/names/star*.txt'):format(archive), api.nvim_buf_get_name(0)) + eq({ 'content of star*.txt' }, lines()) + + for _, name in ipairs(names) do + edit(('zip://%s/names/%s'):format(archive, name)) + eq({ 'content of ' .. name }, lines()) + end + end) + end) + + it('prompts for the password of an encrypted archive', function() + local archive = stage(fixtures, 'encrypted.zip') + clear_zip() + + edit(archive) + eq({ 'secret.txt' }, lines()) + + -- The password is queued first, so that the prompt consumes it from typeahead. + exec_lua(function(uri) + vim.api.nvim_input('hunter2') + vim.api.nvim_cmd({ cmd = 'edit', args = { uri }, magic = { file = false, bar = false } }, {}) + end, ('zip://%s/secret.txt'):format(archive)) + poke_eventloop() + + eq({ 'secret content' }, lines()) + end) + + it('reports an incorrect archive password', function() + local archive = stage(fixtures, 'encrypted.zip') + clear_zip() + + -- Info-ZIP allows three attempts before giving up. Failure msg is scheduled, so wait() for it. + local reported = exec_lua(function(uri) + vim.api.nvim_input('no1no2no3') + vim.api.nvim_cmd({ cmd = 'edit', args = { uri }, magic = { file = false, bar = false } }, {}) + return vim.wait(1000, function() + return vim.api + .nvim_exec2('messages', { output = true }).output + :find('incorrect password', 1, true) ~= nil + end) + end, ('zip://%s/secret.txt'):format(archive)) + + eq(true, reported) + end) + + describe('extract', function() + --- Stage an archive, restart with the cwd inside the test directory, and open it. + local function open_in_cwd(source_dir, source) + local archive = stage(source_dir, source) + clear_zip() + api.nvim_set_current_dir(root) + edit(archive) + return archive + end + + it('extracts the entry under the cursor into the current directory', function() + open_in_cwd(fixtures, 'browser.zip') + feed('') + poke_eventloop() + api.nvim_win_set_cursor(0, { line_of('root.java'), 0 }) + feed('x') + poke_eventloop() + + eq('class root {}\n', t.read_file(vim.fs.joinpath(root, 'root.java'))) + end) + + it('refuses to extract a directory', function() + open_in_cwd(fixtures, 'browser.zip') + api.nvim_win_set_cursor(0, { line_of('folder/'), 0 }) + feed('x') + poke_eventloop() + + eq(true, exec_capture('messages'):find('not a directory', 1, true) ~= nil) + eq(nil, vim.uv.fs_stat(vim.fs.joinpath(root, 'folder'))) + end) + + it('refuses to overwrite an existing file when extracting', function() + local target = vim.fs.joinpath(root, 'root.java') + stage(fixtures, 'browser.zip') + t.write_file(target, 'untouched', true) + clear_zip() + api.nvim_set_current_dir(root) + + edit(vim.fs.joinpath(root, 'browser.zip')) + feed('') + poke_eventloop() + api.nvim_win_set_cursor(0, { line_of('root.java'), 0 }) + feed('x') + poke_eventloop() + + eq(true, exec_capture('messages'):find('already exists', 1, true) ~= nil) + eq('untouched', t.read_file(target)) + end) + + it('extracts suspicious entries without escaping the current directory', function() + open_in_cwd(old_samples, 'evil.zip') + + eq({ + '../../../../etc/ax-pwn', + 'a/../../../../../../../../../../../../../../../../../../tmp/foobar', + '/tmp/vim_zip/a/b/payload.txt', + }, lines()) + + for _, entry in ipairs(lines()) do + api.nvim_win_set_cursor(0, { line_of(entry), 0 }) + feed('x') + poke_eventloop() + end + + -- Each lands flat in the cwd, never at the path the entry asked for. + for _, name in ipairs({ 'ax-pwn', 'foobar', 'payload.txt' }) do + eq(true, vim.uv.fs_stat(vim.fs.joinpath(root, name)) ~= nil) + end + eq(nil, vim.uv.fs_stat(vim.fs.joinpath(root, '..', 'ax-pwn'))) + end) + end) +end) diff --git a/tests/testdata/lua_neovim_zip_spec_change/old.lua b/tests/testdata/lua_neovim_zip_spec_change/old.lua new file mode 100644 index 0000000..088f244 --- /dev/null +++ b/tests/testdata/lua_neovim_zip_spec_change/old.lua @@ -0,0 +1,527 @@ +local n = require('test.functional.testnvim')() +local t = require('test.testutil') + +local describe, it, before_each, after_each = t.describe, t.it, t.before_each, t.after_each +local api = n.api +local eq = t.eq +local exec_capture = n.exec_capture +local exec_lua = n.exec_lua +local feed = n.feed +local fn = n.fn +local poke_eventloop = n.poke_eventloop + +local fixtures = vim.fs.joinpath(t.paths.test_source_path, 'test/functional/fixtures/zip') +local old_samples = vim.fs.joinpath(t.paths.test_source_path, 'test/old/testdir/samples') + +local function lines() + return api.nvim_buf_get_lines(0, 0, -1, false) +end + +local function edit(path) + api.nvim_cmd({ cmd = 'edit', args = { path }, magic = { file = false, bar = false } }, {}) +end + +local function clear_zip() + n.clear({ args = { '--clean' } }) +end + +local function line_of(text) + for i, line in ipairs(lines()) do + if line == text then + return i + end + end + error(('missing line %q in %s'):format(text, vim.inspect(lines()))) +end + +describe('nvim.zip', function() + local root + + --- Copy an archive into the test directory and return its path. + local function stage(source_dir, source, as) + local target = vim.fs.joinpath(root, as or source) + assert(vim.uv.fs_copyfile(vim.fs.joinpath(source_dir, source), target)) + return target + end + + --- Stage the browsing fixture, restart, and open it. + local function browse(as) + local archive = stage(fixtures, 'browser.zip', as) + clear_zip() + edit(archive) + return archive + end + + before_each(function() + t.skip(vim.fn.executable('unzip') == 0, 'unzip not available') + root = vim.fs.normalize(t.tmpname(false) .. ' space%#') + t.mkdir(root) + end) + + after_each(function() + n.rmdir(root) + end) + + describe('activation', function() + it('uses zip.lua by default', function() + browse() + + eq('folder/', lines()[1]) + eq(true, exec_lua('return vim.g.loaded_nvim_zip_plugin == true')) + eq(false, exec_lua('return vim.g.loaded_zipPlugin ~= nil')) + end) + + it('defers to zipPlugin.vim loaded before startup plugins', function() + local archive = stage(old_samples, 'test.zip', 'legacy.zip') + n.clear({ args = { '--clean', '--cmd', 'packadd old-zip' } }) + + edit(archive) + + eq(true, lines()[1]:find('" zip.vim version', 1, true) ~= nil) + eq(0, fn.exists('#nvim.zip')) + eq(false, exec_lua('return vim.g.loaded_nvim_zip_plugin ~= nil')) + end) + + it('yields to zipPlugin.vim loaded after startup', function() + local archive = stage(old_samples, 'test.zip', 'legacy.zip') + clear_zip() + exec_lua([[vim.cmd.packadd('old-zip')]]) + + edit(archive) + + eq(true, lines()[1]:find('" zip.vim version', 1, true) ~= nil) + eq(1, fn.exists('#nvim.zip')) + eq(true, exec_lua('return vim.g.loaded_nvim_zip_plugin == true')) + end) + + it('can be disabled', function() + local archive = stage(fixtures, 'browser.zip') + n.clear({ + args = { '--clean', '--cmd', 'let g:loaded_nvim_zip_plugin = 1' }, + }) + + edit(archive) + + eq(0, fn.exists('#nvim.zip')) + eq('', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('can source zip.lua repeatedly', function() + clear_zip() + + -- Re-sourcing must reuse the augroup rather than stack duplicate autocmds. + eq( + 2, + exec_lua(function() + vim.cmd.runtime('plugin/zip.lua') + vim.cmd.runtime('plugin/zip.lua') + local ids = {} + for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ group = 'nvim.zip' })) do + ids[autocmd.id] = true + end + return vim.tbl_count(ids) + end) + ) + end) + + it('reports an unavailable backend without claiming the buffer', function() + local archive = stage(fixtures, 'browser.zip') + clear_zip() + exec_lua([[vim.env.PATH = '']]) + + edit(archive) + poke_eventloop() + + eq(true, exec_capture('messages'):find('unzip executable not found', 1, true) ~= nil) + eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) + end) + end) + + describe('browsing', function() + it('opens zip-compatible file types', function() + browse('browser.jar') + + eq('folder/', lines()[1]) + eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('browses directories and opens entries', function() + browse() + + eq({ + 'folder/', + 'inner.zip', + '../escape.txt', + '/absolute.txt', + 'crlf.txt', + 'noeol.txt', + 'names/', + }, lines()) + eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) + eq(true, exec_capture('syntax list zipDirectory'):find('zipDirectory', 1, true) ~= nil) + + feed('') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + + feed('') + poke_eventloop() + eq({ 'file.txt' }, lines()) + + feed('-') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + eq('nested/', api.nvim_get_current_line()) + + api.nvim_win_set_cursor(0, { 2, 0 }) + feed('') + poke_eventloop() + eq({ 'root text' }, lines()) + eq('nowrite', api.nvim_get_option_value('buftype', { buf = 0 })) + eq(true, api.nvim_get_option_value('readonly', { buf = 0 })) + eq(false, api.nvim_get_option_value('modifiable', { buf = 0 })) + eq(false, api.nvim_get_option_value('swapfile', { buf = 0 })) + end) + + it('opens the containing directory from the archive root', function() + browse() + feed('-') + poke_eventloop() + + eq(root, vim.fs.normalize(api.nvim_buf_get_name(0))) + eq('browser.zip', api.nvim_get_current_line()) + eq('directory', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('opens entries at quickfix locations', function() + local archive = stage(fixtures, 'browser.zip') + clear_zip() + local uri = ('zip://%s/crlf.txt'):format(archive) + fn.setqflist({}, 'r', { items = { { filename = uri, lnum = 2, col = 1 } } }) + + api.nvim_cmd({ cmd = 'cfirst' }, {}) + + eq(uri, api.nvim_buf_get_name(0)) + eq({ 2, 0 }, api.nvim_win_get_cursor(0)) + eq('text', api.nvim_get_option_value('filetype', { buf = 0 })) + end) + + it('does not reinterpret an entry ending in .zip as an archive', function() + browse() + api.nvim_win_set_cursor(0, { line_of('inner.zip'), 0 }) + feed('') + poke_eventloop() + + eq({ 'nested payload' }, lines()) + eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) + end) + + it('preserves normal file reading details for entries', function() + local archive = stage(fixtures, 'browser.zip') + clear_zip() + + edit(('zip://%s/crlf.txt'):format(archive)) + eq({ 'one', 'two' }, lines()) + eq('dos', api.nvim_get_option_value('fileformat', { buf = 0 })) + eq(true, api.nvim_get_option_value('endofline', { buf = 0 })) + + edit(('zip://%s/noeol.txt'):format(archive)) + eq({ 'no final newline' }, lines()) + eq(false, api.nvim_get_option_value('endofline', { buf = 0 })) + end) + + it('keeps suspicious entry paths visible and readable', function() + browse() + api.nvim_win_set_cursor(0, { line_of('../escape.txt'), 0 }) + feed('') + poke_eventloop() + + eq({ 'escape payload' }, lines()) + end) + + it('opens non-zip files normally', function() + local archive = vim.fs.joinpath(root, 'plain.zip') + t.write_file(archive, 'plain text', true) + clear_zip() + + edit(archive) + + eq({ 'plain text' }, lines()) + eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) + end) + + it('lists a valid empty archive as empty', function() + local archive = vim.fs.joinpath(root, 'empty.zip') + local file = assert(io.open(archive, 'wb')) + -- An end-of-central-directory record with no entries: a valid, empty archive. + file:write('PK\5\6' .. string.rep('\0', 18)) + file:close() + clear_zip() + + edit(archive) + + eq({ '' }, lines()) + eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) + eq(true, exec_lua('return vim.b.nvim_zip ~= nil')) + end) + + it('keeps the current level when reload fails', function() + local archive = browse() + feed('') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + + assert(os.remove(archive)) + feed('R') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + + stage(fixtures, 'browser.zip') + feed('R') + poke_eventloop() + eq({ 'nested/', 'root.txt', 'root.java' }, lines()) + end) + + it('integrates Java sources with zip.lua and zipPlugin.vim', function() + local archive = stage(fixtures, 'browser.zip', 'source.jar') + + for _, legacy in ipairs({ false, true }) do + if legacy then + n.clear({ args = { '--clean', '--cmd', 'packadd old-zip' } }) + else + clear_zip() + end + exec_lua('vim.g.ftplugin_java_source_path = ...', archive) + api.nvim_buf_set_name(0, vim.fs.joinpath(root, 'Test.java')) + api.nvim_buf_set_lines(0, 0, -1, false, { 'folder.root' }) + api.nvim_cmd({ cmd = 'setfiletype', args = { 'java' } }, {}) + api.nvim_cmd({ cmd = 'runtime', args = { 'ftplugin/java.vim' } }, {}) + + feed('gf') + poke_eventloop() + + local uri = legacy and ('zipfile://%s::folder/root.java'):format(archive) + or ('zip://%s/folder/root.java'):format(archive) + eq(uri, api.nvim_buf_get_name(0)) + eq({ 'class root {}' }, lines()) + eq('java', api.nvim_get_option_value('filetype', { buf = 0 })) + end + end) + end) + + -- Info-ZIP expands globs itself and reads leading dashes as options, so archive + -- and entry paths must always reach it as literal text. + describe('backend arguments', function() + it('reads entry selectors literally', function() + t.skip(t.is_os('win'), 'N/A: archive contains backslashes in entry paths') + local archive = stage(old_samples, 'testa.zip', 'special.zip') + clear_zip() + + local cases = { + { 'zipglob/a[a].txt', 'a test file with []' }, + { 'zipglob/a*.txt', 'a test file with a*' }, + { 'zipglob/a?.txt', 'a test file with a?' }, + { [[zipglob/a\.txt]], [[a test file with a\]] }, + { [[zipglob/a\\.txt]], [[a test file with a double \]] }, + } + for _, case in ipairs(cases) do + edit(('zip://%s/%s'):format(archive, case[1])) + eq({ case[2] }, lines()) + end + end) + + it('runs the backend when the cwd is its directory', function() + t.skip(t.is_os('win'), 'N/A: Windows searches the cwd before $PATH') + local archive = stage(fixtures, 'browser.zip') + local bin = vim.fs.joinpath(assert(vim.uv.fs_realpath(root)), 'bin') + t.mkdir(bin) + assert(vim.uv.fs_symlink(vim.fn.exepath('unzip'), vim.fs.joinpath(bin, 'unzip'))) + n.clear({ args = { '--clean' }, env = { PATH = bin .. ':' .. os.getenv('PATH') } }) + + api.nvim_set_current_dir(bin) + edit(archive) + eq('folder/', lines()[1]) + end) + + it('keeps a leading dash in a path from becoming a backend option', function() + local archive = stage(old_samples, 'poc.zip') + clear_zip() + + edit(archive) + eq({ '-d/', 'pwned' }, lines()) + + edit(('zip://%s/-d/tmp'):format(archive)) + eq({ '' }, lines()) + end) + + it('treats archive glob characters literally', function() + t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') + local archive = stage(fixtures, 'browser.zip', 'archive::[*?].zip') + -- Would be matched instead of the archive above if the name were globbed. + stage(old_samples, 'test.zip', 'archivex.zip') + clear_zip() + + edit(archive) + + eq('folder/', lines()[1]) + api.nvim_win_set_cursor(0, { line_of('inner.zip'), 0 }) + feed('') + poke_eventloop() + eq({ 'nested payload' }, lines()) + + local uri = ('zip://%s/crlf.txt'):format(archive) + edit(uri) + eq(uri, api.nvim_buf_get_name(0)) + eq({ 'one', 'two' }, lines()) + end) + + it('lists and opens entries with difficult names', function() + t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') + local archive = browse() + + -- "star*" and "-dash" are the glob and option cases; the rest are shell + -- metacharacters that must survive an argv-based backend untouched. + local names = { + 'space name.txt', + 'two spaces.txt', + ' leading.txt', + 'trailing .txt', + [[back\slash.txt]], + [[single'quote.txt]], + 'double"quote.txt', + 'dollar$name.txt', + 'backtick`name.txt', + 'semi;name.txt', + 'pipe|name.txt', + 'amp&name.txt', + 'paren(name).txt', + 'bang!name.txt', + 'hash#name.txt', + 'percent%name.txt', + 'star*.txt', + 'question?.txt', + '[bracket].txt', + '-dash.txt', + } + + api.nvim_win_set_cursor(0, { line_of('names/'), 0 }) + feed('') + poke_eventloop() + eq(names, lines()) + + api.nvim_win_set_cursor(0, { line_of('star*.txt'), 0 }) + feed('') + poke_eventloop() + eq(('zip://%s/names/star*.txt'):format(archive), api.nvim_buf_get_name(0)) + eq({ 'content of star*.txt' }, lines()) + + for _, name in ipairs(names) do + edit(('zip://%s/names/%s'):format(archive, name)) + eq({ 'content of ' .. name }, lines()) + end + end) + end) + + it('prompts for the password of an encrypted archive', function() + local archive = stage(fixtures, 'encrypted.zip') + clear_zip() + + edit(archive) + eq({ 'secret.txt' }, lines()) + + -- The password is queued first, so that the prompt consumes it from typeahead. + exec_lua(function(uri) + vim.api.nvim_input('hunter2') + vim.api.nvim_cmd({ cmd = 'edit', args = { uri }, magic = { file = false, bar = false } }, {}) + end, ('zip://%s/secret.txt'):format(archive)) + poke_eventloop() + + eq({ 'secret content' }, lines()) + end) + + it('reports an incorrect archive password', function() + local archive = stage(fixtures, 'encrypted.zip') + clear_zip() + + -- Info-ZIP allows three attempts before giving up. + exec_lua(function(uri) + vim.api.nvim_input('no1no2no3') + vim.api.nvim_cmd({ cmd = 'edit', args = { uri }, magic = { file = false, bar = false } }, {}) + end, ('zip://%s/secret.txt'):format(archive)) + poke_eventloop() + + eq(true, exec_capture('messages'):find('incorrect password', 1, true) ~= nil) + end) + + describe('extract', function() + --- Stage an archive, restart with the cwd inside the test directory, and open it. + local function open_in_cwd(source_dir, source) + local archive = stage(source_dir, source) + clear_zip() + api.nvim_set_current_dir(root) + edit(archive) + return archive + end + + it('extracts the entry under the cursor into the current directory', function() + open_in_cwd(fixtures, 'browser.zip') + feed('') + poke_eventloop() + api.nvim_win_set_cursor(0, { line_of('root.java'), 0 }) + feed('x') + poke_eventloop() + + eq('class root {}\n', t.read_file(vim.fs.joinpath(root, 'root.java'))) + end) + + it('refuses to extract a directory', function() + open_in_cwd(fixtures, 'browser.zip') + api.nvim_win_set_cursor(0, { line_of('folder/'), 0 }) + feed('x') + poke_eventloop() + + eq(true, exec_capture('messages'):find('not a directory', 1, true) ~= nil) + eq(nil, vim.uv.fs_stat(vim.fs.joinpath(root, 'folder'))) + end) + + it('refuses to overwrite an existing file when extracting', function() + local target = vim.fs.joinpath(root, 'root.java') + stage(fixtures, 'browser.zip') + t.write_file(target, 'untouched', true) + clear_zip() + api.nvim_set_current_dir(root) + + edit(vim.fs.joinpath(root, 'browser.zip')) + feed('') + poke_eventloop() + api.nvim_win_set_cursor(0, { line_of('root.java'), 0 }) + feed('x') + poke_eventloop() + + eq(true, exec_capture('messages'):find('already exists', 1, true) ~= nil) + eq('untouched', t.read_file(target)) + end) + + it('extracts suspicious entries without escaping the current directory', function() + open_in_cwd(old_samples, 'evil.zip') + + eq({ + '../../../../etc/ax-pwn', + 'a/../../../../../../../../../../../../../../../../../../tmp/foobar', + '/tmp/vim_zip/a/b/payload.txt', + }, lines()) + + for _, entry in ipairs(lines()) do + api.nvim_win_set_cursor(0, { line_of(entry), 0 }) + feed('x') + poke_eventloop() + end + + -- Each lands flat in the cwd, never at the path the entry asked for. + for _, name in ipairs({ 'ax-pwn', 'foobar', 'payload.txt' }) do + eq(true, vim.uv.fs_stat(vim.fs.joinpath(root, name)) ~= nil) + end + eq(nil, vim.uv.fs_stat(vim.fs.joinpath(root, '..', 'ax-pwn'))) + end) + end) +end)