├── .github └── workflows │ ├── style.yml │ └── test.yml ├── .gitignore ├── .styluaignore ├── CONFIG.md ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── compare.lua └── preload.vim ├── lua ├── nvim-yati.lua └── nvim-yati │ ├── config.lua │ ├── configs │ ├── c.lua │ ├── comment.lua │ ├── cpp.lua │ ├── css.lua │ ├── graphql.lua │ ├── html.lua │ ├── javascript.lua │ ├── jsdoc.lua │ ├── json.lua │ ├── json5.lua │ ├── lua.lua │ ├── python.lua │ ├── rust.lua │ ├── styled.lua │ ├── toml.lua │ ├── tsx.lua │ ├── typescript.lua │ └── vue.lua │ ├── context.lua │ ├── fallback.lua │ ├── handlers │ ├── common.lua │ ├── default.lua │ ├── init.lua │ └── rust.lua │ ├── indent.lua │ ├── internal.lua │ ├── logger.lua │ └── utils.lua ├── plugin └── nvim-yati.vim ├── stylua.toml └── tests ├── fixtures ├── c │ └── sample.c ├── cpp │ └── sample.cpp ├── css │ └── sample.css ├── graphql │ └── sample.graphql ├── html │ └── sample.html ├── javascript │ ├── arrow_func_in_args.js │ ├── basic.js │ ├── binary.js │ ├── chained_call.js │ ├── iife.js │ ├── injection.js │ ├── jsx.js │ ├── jsx_ternary.fail.js │ ├── statements.js │ └── ternary.js ├── json │ └── sample.json ├── lua │ └── sample.lua ├── python │ ├── nested_align.py │ └── sample.py ├── rust │ ├── micro.rs │ └── sample.rs ├── toml │ └── sample.toml ├── tsx │ └── sample.tsx ├── typescript │ ├── basic.ts │ └── return_type.ts └── vue │ └── sample.vue ├── helper.lua ├── indent_spec.lua ├── install.vim ├── lazy_indent_spec.lua └── preload.vim /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: Style 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | stylua: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: JohnnyMorganz/stylua-action@v1 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | version: latest 18 | args: --check . 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: "0 20 * * 1" 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: ["ubuntu-latest"] 17 | nvim-versions: ["stable", "nightly"] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v2 22 | - name: Setup neovim 23 | uses: rhysd/action-setup-vim@v1 24 | with: 25 | neovim: true 26 | version: ${{ matrix.nvim-versions }} 27 | - name: Install deps 28 | run: make deps 29 | - name: Install parsers 30 | run: nvim --headless -u "tests/install.vim" -c "q" 31 | - name: Run test 32 | run: make test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | bench_sample.lua 3 | -------------------------------------------------------------------------------- /.styluaignore: -------------------------------------------------------------------------------- 1 | tests/fixtures/ 2 | benchmark/sample.lua 3 | -------------------------------------------------------------------------------- /CONFIG.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | **NOTE**: All contents here are highly unstable. 4 | 5 | ## Node Attributes 6 | 7 | ### scope 8 | 9 | Types of nodes considered as an indent scope. Direct children of them should be indented one more level than the parent, except the first and last child, usually open and close delimiters. 10 | 11 | ```lua 12 | function fn(fd) 13 | -- I'm indented 14 | end -- Don't indent the last 'end' delimiter 15 | ``` 16 | 17 | ### scope_open 18 | 19 | Almost the same as `scope` except the last child should also be indented. This usually applies to nodes with only open delimiter. 20 | 21 | ```c 22 | if (1) 23 | some_call() // should be indented 24 | ``` 25 | 26 | ### scope_open_extended 27 | 28 | Same as `scope_open` but the range of the node should be considered 'extended' to cover following empty lines. 29 | 30 | ```python 31 | if True: 32 | 33 | # extended and should be indented 34 | ``` 35 | 36 | ### dedent_child 37 | 38 | List of type of nodes denote the direct children which should not be indented of the indent nodes in `scope` and `scope_open`. 39 | 40 | ```lua 41 | if 42 | true 43 | then -- 'then' should be added to 'dedent_child' of 'if_statement' 44 | -- I'm indented 45 | end 46 | ``` 47 | 48 | ### indent_zero 49 | 50 | The node should be zero indented. Used especially to dedent macros in C to 0. 51 | 52 | ```c 53 | { 54 | { 55 | #if 1 56 | // normal indent 57 | #endif 1 58 | } 59 | } 60 | ``` 61 | 62 | ### indent_align 63 | 64 | Used especially to align node to open delimiter in Python. 65 | 66 | ```python 67 | def fun(a, 68 | b): # aligned indent to open delimiter of arguments 69 | pass 70 | ``` 71 | 72 | ### indent_fallback 73 | 74 | Compute indent by fallback method for this type of node. By default, 'ERROR' node is always denoted as `indent_fallback` because it cannot be handled by tree-sitter. 75 | 76 | ### indent_list 77 | 78 | EXPERIMENTAL. I cannot figure out an accurate description for this so just ignore this section. 79 | 80 | ```javascript 81 | someCall({ 82 | a, 83 | }, [ 84 | b 85 | ], () => { 86 | foo(); 87 | }); 88 | ``` 89 | 90 | ### ignore 91 | 92 | Nodes considered not exist but their children should be remained. This is similar to an unwrap operation on the node to release its children directly to its parent. Some tree-sitter syntax wraps nodes extra levels and we might want to unwrap them to make the indent calculated correctly. 93 | 94 | ```javascript 95 | const jsx = ( 96 |
97 |
98 | 'jsx_text' should be unwraped to 'jsx_element' 99 |
100 |
101 | ); 102 | ``` 103 | 104 | ## Handlers 105 | 106 | The function signature is `fun(ctx: YatiContext): boolean|nil`. 107 | 108 | For the return value, 109 | 110 | - `true`: **Handled**, but continue traversing up 111 | - `false`: **Handled**, and stop traversing 112 | - `nil`: Not handled, try other handlers 113 | 114 | For the two types of handlers, 115 | 116 | - `on_initial`: On the very beginning when the base indent node is not decided yet. 117 | - `on_traverse`: On the traversal process from bottom to up. 118 | 119 | For the type of context and available field, refer to [context.lua](./lua/nvim-yati/context.lua). 120 | 121 | Example handler: 122 | 123 | ```lua 124 | function break_on_error_node(ctx) 125 | if ctx.node:type() == "ERROR" then 126 | ctx:set(-1) 127 | -- or return ctx:fallback() to use fallback method 128 | return false 129 | end 130 | end 131 | ``` 132 | 133 | ## Fallback Method 134 | 135 | The function signature is `fun(lnum: integer, computed: integer, bufnr: integer): integer`. 136 | 137 | **NOTE**: Value of `computed` should be added to indent of `lnum` calculated by fallback method (unless you deliberately return -1 to use auto indent of vim). 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 yioneko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | DEPS_CLONE_DIR:=deps/pack/vendor/start 3 | 4 | deps: 5 | @mkdir -p ${DEPS_CLONE_DIR} 6 | git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ${DEPS_CLONE_DIR}/plenary.nvim 7 | git clone --depth 1 https://github.com/nvim-treesitter/nvim-treesitter ${DEPS_CLONE_DIR}/nvim-treesitter 8 | 9 | test: deps 10 | @nvim \ 11 | --headless \ 12 | --noplugin \ 13 | -u tests/install.vim \ 14 | -c "PlenaryBustedDirectory tests/ { minimal_init = 'tests/preload.vim' }" 15 | 16 | BENCH_SAMPLE := bench_sample.lua 17 | $(BENCH_SAMPLE): 18 | curl -o $(BENCH_SAMPLE) https://raw.githubusercontent.com/neovim/neovim/master/runtime/lua/vim/lsp.lua 19 | 20 | bench: deps $(BENCH_SAMPLE) 21 | @nvim \ 22 | --headless \ 23 | --noplugin \ 24 | -u benchmark/preload.vim \ 25 | -c "lua require('benchmark.compare').run()" 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-yati 2 | 3 | Yet another tree-sitter indent plugin for Neovim. 4 | 5 | This plugin was originally created when the experience of builtin indent module of [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) was still terrible. Now since it has improved a lot with better community support, this plugin **should be no longer needed** if the upstream one already satisfies you. 6 | 7 | If you are still frustrated with the 'official' indent module or interested in this plugin, welcome to provide feedback or submit any issues. Take a glance at [features](#features) to learn about the differences. 8 | 9 |
10 | 11 | Supported languages 12 | 13 | 14 | - C/C++ 15 | - CSS 16 | - GraphQL 17 | - HTML 18 | - Javascript/Typescript (jsx and tsx are also supported) 19 | - JSON 20 | - Lua 21 | - Python 22 | - Rust 23 | - TOML 24 | - Vue 25 | 26 |
27 | 28 | More languages could be supported by [setup](#setup) or adding config files to [configs/](lua/nvim-yati/configs) directory. 29 | 30 | ## Compatibility 31 | 32 | This plugin is always developed based on latest neovim and nvim-treesitter. Please consider upgrading them if there is any compatibility issue. 33 | 34 | The plugin has been completely rewritten since `legacy` tag. Use that if you prefer not migrating to the current version for some reason. 35 | 36 | ## Installation 37 | 38 | [packer.nvim](https://github.com/wbthomason/packer.nvim): 39 | 40 | ```lua 41 | use({ "yioneko/nvim-yati", tag = "*", requires = "nvim-treesitter/nvim-treesitter" }) 42 | ``` 43 | 44 | [vim-plug](https://github.com/junegunn/vim-plug): 45 | 46 | ```vim 47 | Plug "nvim-treesitter/nvim-treesitter" 48 | Plug "yioneko/nvim-yati", { 'tag': '*' } 49 | ``` 50 | 51 | ## Setup 52 | 53 | The module is **required** to be enabled to work: 54 | 55 | ```lua 56 | require("nvim-treesitter.configs").setup { 57 | yati = { 58 | enable = true, 59 | -- Disable by languages, see `Supported languages` 60 | disable = { "python" }, 61 | 62 | -- Whether to enable lazy mode (recommend to enable this if bad indent happens frequently) 63 | default_lazy = true, 64 | 65 | -- Determine the fallback method used when we cannot calculate indent by tree-sitter 66 | -- "auto": fallback to vim auto indent 67 | -- "asis": use current indent as-is 68 | -- "cindent": see `:h cindent()` 69 | -- Or a custom function return the final indent result. 70 | default_fallback = "auto" 71 | }, 72 | indent = { 73 | enable = false -- disable builtin indent module 74 | } 75 | } 76 | ``` 77 | 78 | I also created an accompanying regex-based indent plugin ([vim-tmindent](https://github.com/yioneko/vim-tmindent)) to support saner fallback indent calculation, which could be a drop-in replacement of builtin indent method of vim. See [integration](https://github.com/yioneko/vim-tmindent#nvim-yati) for its fallback setup. 79 | 80 | If you want to use the indent module simultaneously, disable the indent module for languages to be handled by this plugin. 81 | 82 | ```lua 83 | require("nvim-treesitter.configs").setup { 84 | indent = { 85 | enable = true, 86 | disable = { "html", "javascript" } 87 | }, 88 | -- And optionally, disable the conflict warning emitted by plugin 89 | yati = { 90 | suppress_conflict_warning = true, 91 | }, 92 | } 93 | ``` 94 | 95 | Example for a more customized setup: 96 | 97 | ```lua 98 | local get_builtin = require("nvim-yati.config").get_builtin 99 | -- This is just an example, not recommend to do this since the result is unpredictable 100 | local js_overrides = vim.tbl_deep_extend("force", get_builtin("javascript"), { 101 | lazy = false, 102 | fallback = function() return -1 end, 103 | nodes = { 104 | ["if_statement"] = { "scope" }, -- set attributes by node 105 | }, 106 | handlers = { 107 | on_initial = {}, 108 | on_travers = { 109 | function(ctx) return false end, -- set custom handlers 110 | } 111 | } 112 | }) 113 | 114 | require("nvim-treesitter.configs").setup { 115 | yati = { 116 | enable = true, 117 | disable = { "python" }, 118 | default_lazy = false, 119 | default_fallback = function() return -1 end, -- provide custom fallback indent method 120 | overrides = { 121 | javascript = js_overrides -- override config by language 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | More technical details goes there (**highly unstable**): [CONFIG.md](./CONFIG.md). 128 | 129 | ## Features 130 | 131 | - Fast, match node on demand by implementing completely in Lua, compared to executing scm query on the whole tree on every indent calculation. 132 | - Could be faster and more context aware if `lazy` enabled, see `default_lazy` option. This is specifically useful if the surrounding code doesn't obey indent rules: 133 | 134 | ```lua 135 | function fun() 136 | if abc then 137 | if cbd then 138 | a() -- new indent will goes here even if the parent node indent wrongly 139 | end 140 | end 141 | end 142 | ``` 143 | 144 | - Fallback indent method support to reuse calculated indent from tree. 145 | - Support indent in injection region. See [sample.html](tests/fixtures/html/sample.html) for example. 146 | - [Tests](tests/fixtures) covered and handles much more edge cases. Refer samples in that directory for what the indentation would be like. The style is slightly opinionated as there is no actual standard, but customization is still possible. 147 | - Support for custom handlers to deal with complex scenarios. This plugin relies on dedicated handlers to fix many edge cases like the following one: 148 | 149 | ```python 150 | if True: 151 | pass 152 | else: # should auto dedent <- 153 | # the parsed tree is broken here and cannot be handled by tree-sitter 154 | ``` 155 | 156 | ## Notes 157 | 158 | - The calculation result heavily relies on the correct tree-sitter parsing of the code. I'd recommend using plugins like [nvim-autopairs](https://github.com/windwp/nvim-autopairs) or [luasnip](https://github.com/L3MON4D3/LuaSnip) to keep the syntax tree error-free while editing. This should avoid most of the wrong indent calculations. 159 | - I mainly write javascript so other languages may not receive better support than it, and bad cases for other languages are generally expected. Please create issues for them if possible. 160 | 161 | ## Credits 162 | 163 | - [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) for initial aspiration and test cases. 164 | - [chfritz/atom-sane-indentation](https://github.com/chfritz/atom-sane-indentation) for algorithm and test cases. 165 | -------------------------------------------------------------------------------- /benchmark/compare.lua: -------------------------------------------------------------------------------- 1 | local yati = require("nvim-yati.indent").indentexpr 2 | local nvim_ts = require("nvim-treesitter.indent").get_indent 3 | local bench = require("plenary.benchmark") 4 | 5 | local sample_file = "bench_sample.lua" 6 | 7 | local M = {} 8 | 9 | local function test_indent(get_indent) 10 | local lines = vim.api.nvim_buf_get_lines(0, 2000, 2200, false) 11 | for i, line in ipairs(lines) do 12 | if vim.trim(line) ~= "" then 13 | -- simulate editing operation to invalidate current syntax tree 14 | vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { "" }) 15 | 16 | local computed = get_indent(i) 17 | end 18 | end 19 | end 20 | 21 | local function run_test() 22 | vim.cmd("edit! " .. sample_file) 23 | vim.bo.shiftwidth = 2 24 | 25 | bench("nvim_ts", { 26 | runs = 10, 27 | fun = { 28 | { 29 | "nvim_ts", 30 | function() 31 | test_indent(nvim_ts) 32 | end, 33 | }, 34 | }, 35 | }) 36 | bench("yati", { 37 | runs = 10, 38 | fun = { 39 | { 40 | "yati", 41 | function() 42 | test_indent(yati) 43 | end, 44 | }, 45 | }, 46 | }) 47 | end 48 | 49 | function M.run() 50 | run_test() 51 | vim.cmd("qall!") 52 | end 53 | 54 | return M 55 | -------------------------------------------------------------------------------- /benchmark/preload.vim: -------------------------------------------------------------------------------- 1 | set noswapfile 2 | set packpath+=./deps 3 | set rtp+=. 4 | 5 | packloadall 6 | -------------------------------------------------------------------------------- /lua/nvim-yati.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.config") 2 | local M = {} 3 | 4 | function M.init() 5 | require("nvim-treesitter").define_modules({ 6 | yati = { 7 | module_path = "nvim-yati.internal", 8 | is_supported = config.is_supported, 9 | overrides = {}, 10 | }, 11 | }) 12 | end 13 | 14 | return M 15 | -------------------------------------------------------------------------------- /lua/nvim-yati/config.lua: -------------------------------------------------------------------------------- 1 | local get_module_config = require("nvim-treesitter.configs").get_module 2 | 3 | local M = {} 4 | 5 | ---@class YatiBuiltinConfig 6 | ---@field scope? string[] 7 | ---@field scope_open? string[] 8 | ---@field scope_open_extended? string[] 9 | ---@field indent_zero? string[] 10 | ---@field indent_align? string[] 11 | ---@field indent_list? string[] 12 | ---@field indent_fallback? string[] 13 | ---@field ignore? string[] 14 | ---@field dedent_child? table 15 | ---@field handlers? YatiHandlers 16 | ---@field fallback? YatiFallback 17 | 18 | ---@class YatiNodeConfig 19 | ---@field scope boolean 20 | ---@field scope_open boolean 21 | ---@field scope_open_extended boolean 22 | ---@field indent_zero boolean 23 | ---@field indent_align boolean 24 | ---@field indent_list boolean 25 | ---@field indent_fallback boolean 26 | ---@field ignore boolean 27 | ---@field dedent_child string[] 28 | 29 | ---@alias YatiNodesConfig table 30 | 31 | ---@class YatiLangConfig 32 | ---@field nodes YatiNodesConfig 33 | ---@field handlers YatiHandlers 34 | ---@field fallback YatiFallback 35 | ---@field lazy boolean 36 | 37 | ---@class YatiUserConfig 38 | ---@field overrides table 39 | ---@field default_fallback nil|YatiFallback 40 | ---@field default_lazy nil|boolean 41 | ---@field suppress_conflict_warning nil|boolean 42 | ---@field suppress_indent_err nil|boolean 43 | 44 | ---@type YatiBuiltinConfig 45 | local common_config = { 46 | scope = {}, 47 | scope_open = {}, 48 | scope_open_extended = {}, 49 | indent_zero = {}, 50 | indent_align = {}, 51 | indent_list = {}, 52 | dedent_child = {}, 53 | -- ignore these outermost nodes to work around cross tree issue 54 | ignore = { "source", "document", "chunk", "script_file", "source_file", "program" }, 55 | fallback = "asis", 56 | indent_fallback = { "ERROR" }, 57 | handlers = { 58 | on_initial = {}, 59 | on_traverse = {}, 60 | }, 61 | } 62 | 63 | local function set_nodes_default_meta(nodes) 64 | setmetatable(nodes, { 65 | __index = function(tbl, key) 66 | rawset(tbl, key, { dedent_child = {} }) 67 | return rawget(tbl, key) 68 | end, 69 | }) 70 | end 71 | 72 | ---@param config YatiBuiltinConfig 73 | ---@return YatiLangConfig 74 | function M.transform_builtin(config) 75 | ---@type YatiLangConfig 76 | local transformed = { nodes = {}, handlers = { on_initial = {}, on_traverse = {} } } 77 | set_nodes_default_meta(transformed.nodes) 78 | 79 | for _, node in ipairs(config.scope) do 80 | transformed.nodes[node].scope = true 81 | end 82 | for _, node in ipairs(config.scope_open) do 83 | transformed.nodes[node].scope = true 84 | transformed.nodes[node].scope_open = true 85 | end 86 | for _, node in ipairs(config.scope_open_extended) do 87 | transformed.nodes[node].scope = true 88 | transformed.nodes[node].scope_open = true 89 | transformed.nodes[node].scope_open_extended = true 90 | end 91 | for _, node in ipairs(config.indent_zero) do 92 | transformed.nodes[node].indent_zero = true 93 | end 94 | for _, node in ipairs(config.indent_align) do 95 | transformed.nodes[node].indent_align = true 96 | end 97 | for _, node in ipairs(config.indent_list) do 98 | transformed.nodes[node].indent_list = true 99 | end 100 | for _, node in ipairs(config.ignore) do 101 | transformed.nodes[node].ignore = true 102 | end 103 | for _, node in ipairs(config.indent_fallback) do 104 | transformed.nodes[node].indent_fallback = true 105 | end 106 | for node, child_list in pairs(config.dedent_child) do 107 | transformed.nodes[node].scope = true 108 | transformed.nodes[node].dedent_child = child_list 109 | end 110 | transformed.handlers.on_traverse = config.handlers.on_traverse or {} 111 | transformed.handlers.on_initial = config.handlers.on_initial or {} 112 | transformed.fallback = config.fallback 113 | -- transformed.lazy = true 114 | 115 | return transformed 116 | end 117 | 118 | ---@param base YatiBuiltinConfig 119 | ---@param config YatiBuiltinConfig 120 | ---@return YatiBuiltinConfig 121 | function M.extend(base, config) 122 | local merged = vim.deepcopy(base) 123 | 124 | vim.list_extend(merged.scope or {}, config.scope or {}) 125 | vim.list_extend(merged.scope_open or {}, config.scope_open or {}) 126 | vim.list_extend(merged.scope_open_extended or {}, config.scope_open_extended or {}) 127 | vim.list_extend(merged.indent_zero or {}, config.indent_zero or {}) 128 | vim.list_extend(merged.indent_align or {}, config.indent_align or {}) 129 | vim.list_extend(merged.indent_list or {}, config.indent_list or {}) 130 | vim.list_extend(merged.indent_fallback or {}, config.indent_fallback or {}) 131 | vim.list_extend(merged.ignore or {}, config.ignore or {}) 132 | if config.handlers then 133 | vim.list_extend(merged.handlers.on_initial or {}, config.handlers.on_initial or {}) 134 | vim.list_extend(merged.handlers.on_traverse or {}, config.handlers.on_traverse or {}) 135 | end 136 | if config.fallback then 137 | merged.fallback = config.fallback 138 | end 139 | merged.dedent_child = vim.tbl_extend("force", merged.dedent_child or {}, config.dedent_child or {}) 140 | 141 | return merged 142 | end 143 | 144 | ---@return YatiUserConfig 145 | function M.get_user_config() 146 | return get_module_config("yati") 147 | end 148 | 149 | ---@param lang string 150 | ---@return boolean 151 | function M.is_supported(lang) 152 | local user_config = M.get_user_config() 153 | if user_config.overrides and user_config.overrides[lang] then 154 | return true 155 | end 156 | local exists = pcall(require, "nvim-yati.configs." .. lang) 157 | return exists 158 | end 159 | 160 | ---@type table 161 | local builtin_lang_config_cache = {} 162 | 163 | ---@param lang string 164 | ---@return YatiLangConfig 165 | function M.get_builtin(lang) 166 | if not builtin_lang_config_cache[lang] then 167 | local ok, config = pcall(require, "nvim-yati.configs." .. lang) 168 | if ok then 169 | builtin_lang_config_cache[lang] = M.transform_builtin(M.extend(common_config, config)) 170 | end 171 | end 172 | return builtin_lang_config_cache[lang] 173 | end 174 | 175 | ---@param lang string 176 | ---@param user_config YatiUserConfig|nil 177 | ---@return YatiLangConfig|nil 178 | function M.get(lang, user_config) 179 | local conf = M.get_builtin(lang) 180 | if not user_config then 181 | return conf 182 | end 183 | local overrides = user_config.overrides 184 | if overrides and overrides[lang] then 185 | conf = vim.tbl_extend("keep", overrides[lang], { 186 | nodes = {}, 187 | handlers = {}, 188 | }) 189 | set_nodes_default_meta(conf.nodes) 190 | conf.handlers.on_initial = conf.handlers.on_initial or {} 191 | conf.handlers.on_traverse = conf.handlers.on_traverse or {} 192 | end 193 | 194 | if not conf then 195 | return 196 | end 197 | 198 | if user_config.default_lazy ~= nil then 199 | conf.lazy = user_config.default_lazy 200 | end 201 | if user_config.default_fallback then 202 | conf.fallback = user_config.default_fallback 203 | end 204 | 205 | return conf 206 | end 207 | 208 | ---@param user_config YatiUserConfig|nil 209 | function M.with_user_config_get(user_config) 210 | ---@type table 211 | local lang_config_cache = {} 212 | 213 | ---@param lang string 214 | ---@return YatiLangConfig|nil 215 | return function(lang) 216 | if lang_config_cache[lang] then 217 | return lang_config_cache[lang] 218 | end 219 | lang_config_cache[lang] = M.get(lang, user_config) 220 | return lang_config_cache[lang] 221 | end 222 | end 223 | 224 | return M 225 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/c.lua: -------------------------------------------------------------------------------- 1 | local ch = require("nvim-yati.handlers.common") 2 | 3 | ---@type YatiBuiltinConfig 4 | local config = { 5 | scope = { 6 | "compound_statement", 7 | "argument_list", 8 | "field_declaration_list", 9 | "enumerator_list", 10 | "parameter_list", 11 | "initializer_list", 12 | "parenthesized_expression", 13 | "preproc_function_def", 14 | "preproc_arg", 15 | }, 16 | scope_open = { 17 | "for_statement", 18 | "if_statement", 19 | "else_clause", 20 | "while_statement", 21 | "do_statement", 22 | "case_statement", 23 | "return_statement", 24 | "shift_expression", 25 | "call_expression", 26 | "field_expression", 27 | "logical_expression", 28 | "math_expression", 29 | "conditional_expression", 30 | "relational_expression", 31 | "assignment_expression", 32 | "field_initializer_list", 33 | "init_declarator", 34 | "concatenated_string", 35 | "binary_expression", 36 | "labeled_statement", 37 | }, 38 | dedent_child = { 39 | compound_statement = { 40 | "labeled_statement", 41 | }, 42 | if_statement = { 43 | "compound_statement", 44 | "if_statement", 45 | "parenthesized_expression", 46 | "'else'", 47 | "else_clause", 48 | }, 49 | else_clause = { "compound_statement", "parenthesized_expression" }, 50 | while_statement = { "compound_statement", "parenthesized_expression" }, 51 | do_statement = { "compound_statement", "parenthesized_expression" }, 52 | for_statement = { "compound_statement", "parenthesized_expression" }, 53 | }, 54 | ignore = { "preproc_if", "preproc_else" }, 55 | indent_zero = { "'#if'", "'#else'", "'#endif'", "'#ifdef'", "'#ifndef'", "'#define'" }, 56 | handlers = { 57 | on_initial = { 58 | ch.block_comment_extra_indent("comment", {}), 59 | }, 60 | }, 61 | } 62 | 63 | return config 64 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/comment.lua: -------------------------------------------------------------------------------- 1 | local ch = require("nvim-yati.handlers.common") 2 | 3 | ---@type YatiBuiltinConfig 4 | local config = { 5 | handlers = { 6 | on_initial = { 7 | ch.block_comment_extra_indent("comment", { "'text'", "source", "description", "document" }), 8 | ch.block_comment_extra_indent("block_comment", { "'text'", "source", "description", "document", "'*/'" }), 9 | }, 10 | }, 11 | } 12 | 13 | return config 14 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/cpp.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.configs.c") 2 | local extend = require("nvim-yati.config").extend 3 | 4 | return extend(config, { 5 | scope = { 6 | "template_parameter_list", 7 | "template_argument_list", 8 | "condition_clause", 9 | }, 10 | scope_open = { 11 | "for_range_loop", 12 | "condition_clause", 13 | "lambda_expression", 14 | "abstract_function_declarator", 15 | "field_initializer_list", 16 | "init_declarator", 17 | "class_specifier", 18 | "if_statement", 19 | "while_statement", 20 | "for_statement", 21 | "for_range_loop", 22 | }, 23 | dedent_child = { 24 | field_declaration_list = { 25 | "access_specifier", 26 | }, 27 | for_range_loop = { "compound_statement" }, 28 | if_statement = { 29 | "compound_statement", 30 | "if_statement", 31 | "condition_clause", 32 | "'else'", 33 | "else_clause", 34 | }, 35 | else_clause = { "compound_statement" }, 36 | while_statement = { "compound_statement", "condition_clause" }, 37 | do_statement = { "compound_statement", "condition_clause" }, 38 | for_statement = { "compound_statement" }, 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/css.lua: -------------------------------------------------------------------------------- 1 | ---@type YatiBuiltinConfig 2 | local config = { 3 | scope = { 4 | "block", 5 | "declaration", 6 | }, 7 | ignore = { 8 | "raw_text", 9 | "stylesheet", 10 | }, 11 | } 12 | 13 | return config 14 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/graphql.lua: -------------------------------------------------------------------------------- 1 | ---@type YatiBuiltinConfig 2 | local config = { 3 | scope = { 4 | "selection_set", 5 | "arguments", 6 | "fields_definition", 7 | "arguments_definition", 8 | "object_value", 9 | "list_value", 10 | "variable_definitions", 11 | "enum_values_definition", 12 | }, 13 | scope_open = { 14 | "union_member_types", 15 | }, 16 | } 17 | 18 | return config 19 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/html.lua: -------------------------------------------------------------------------------- 1 | ---@type YatiBuiltinConfig 2 | local config = { 3 | scope = { 4 | "element", 5 | "style_element", 6 | "script_element", 7 | "start_tag", 8 | "end_tag", 9 | "self_closing_tag", 10 | }, 11 | ignore = { 12 | "raw_text", 13 | }, 14 | } 15 | 16 | return config 17 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/javascript.lua: -------------------------------------------------------------------------------- 1 | local ch = require("nvim-yati.handlers.common") 2 | 3 | ---@type YatiBuiltinConfig 4 | local config = { 5 | scope = { 6 | "array", 7 | "object", 8 | "object_pattern", 9 | "arguments", 10 | "statement_block", 11 | "class_body", 12 | "parenthesized_expression", 13 | "formal_parameters", 14 | "jsx_element", 15 | "jsx_fragment", 16 | "jsx_opening_element", 17 | "jsx_expression", 18 | "switch_body", 19 | "member_expression", 20 | "template_substitution", 21 | "named_imports", 22 | "export_clause", 23 | "subscript_expression", 24 | }, 25 | scope_open = { 26 | "expression_statement", 27 | "variable_declarator", 28 | "lexical_declaration", 29 | "member_expression", 30 | "binary_expression", 31 | "return_statement", 32 | "if_statement", 33 | "else_clause", 34 | "for_statement", 35 | "for_in_statement", 36 | "while_statement", 37 | "jsx_self_closing_element", 38 | "assignment_expression", 39 | "arrow_function", 40 | "call_expression", 41 | "pair", 42 | }, 43 | scope_open_extended = { 44 | "switch_case", 45 | "switch_default", 46 | }, 47 | indent_list = { 48 | "object", 49 | "array", 50 | "arguments", 51 | }, 52 | dedent_child = { 53 | if_statement = { "statement_block", "else_clause", "parenthesized_expression" }, 54 | else_clause = { "statement_block", "parenthesized_expression" }, 55 | while_statement = { "statement_block", "parenthesized_expression" }, 56 | for_statement = { "statement_block", "'('", "')'" }, 57 | for_in_statement = { "statement_block", "'('", "')'" }, 58 | arrow_function = { "statement_block" }, 59 | jsx_fragment = { "'<'" }, 60 | jsx_self_closing_element = { "'/>'" }, 61 | }, 62 | ignore = { "jsx_text" }, 63 | handlers = { 64 | on_initial = { 65 | ch.multiline_string_literal("template_string"), 66 | ch.multiline_string_literal("string_fragment"), 67 | ch.block_comment_extra_indent("comment", {}), 68 | }, 69 | on_traverse = { 70 | ch.ternary_flatten_indent("ternary_expression"), 71 | ch.chained_field_call("arguments", "member_expression", "property"), 72 | ch.multiline_string_injection("template_string", "`"), 73 | ch.multiline_string_injection("string_fragment", "`"), 74 | }, 75 | }, 76 | } 77 | 78 | return config 79 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/jsdoc.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.configs.comment") 2 | local extend = require("nvim-yati.config").extend 3 | 4 | return extend(config, {}) 5 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/json.lua: -------------------------------------------------------------------------------- 1 | ---@type YatiBuiltinConfig 2 | local config = { 3 | scope = { 4 | "array", 5 | "object", 6 | }, 7 | } 8 | 9 | return config 10 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/json5.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.configs.json") 2 | local extend = require("nvim-yati.config").extend 3 | 4 | return extend(config, {}) 5 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/lua.lua: -------------------------------------------------------------------------------- 1 | local ch = require("nvim-yati.handlers.common") 2 | 3 | ---@type YatiBuiltinConfig 4 | local config = { 5 | scope = { 6 | "table_constructor", 7 | "function", 8 | "function_definition", 9 | "function_declaration", 10 | "expression_list", 11 | "parameters", 12 | "arguments", 13 | "if_statement", 14 | "do_statement", 15 | "for_statement", 16 | "for_in_statement", 17 | "while_statement", 18 | "repeat_statement", 19 | "parenthesized_expression", 20 | }, 21 | scope_open = { 22 | "else_statement", 23 | "elseif_statement", 24 | "assignment_statement", 25 | "function_call", 26 | "method_index_expression", 27 | "variable_declaration", 28 | "dot_index_expression", 29 | "return_statement", 30 | }, 31 | indent_list = { 32 | "arguments", 33 | "table_constructor", 34 | }, 35 | dedent_child = { 36 | local_function = { "parameters" }, 37 | function_definition = { "parameters" }, 38 | function_declaration = { "parameters" }, 39 | ["function"] = { "parameters" }, 40 | if_statement = { "'then'", "else_statement", "elseif_statement" }, 41 | elseif_statement = { "'then'" }, 42 | for_statement = { "'do'" }, 43 | for_in_statement = { "'do'", "'in'" }, 44 | while_statement = { "'do'" }, 45 | repeat_statement = { "'until'" }, 46 | }, 47 | ignore = { "binary_expression" }, -- ignore binary_expression to be compatible with stylua 48 | handlers = { 49 | on_initial = { 50 | ch.multiline_string_literal("string_content"), 51 | }, 52 | on_traverse = { 53 | ch.chained_field_call("arguments", "method_index_expression", "method"), 54 | ch.chained_field_call("arguments", "dot_index_expression", "field"), 55 | ch.multiline_string_injection("string_content", "string_end"), 56 | }, 57 | }, 58 | } 59 | 60 | return config 61 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/python.lua: -------------------------------------------------------------------------------- 1 | local ch = require("nvim-yati.handlers.common") 2 | 3 | ---@type YatiBuiltinConfig 4 | local config = { 5 | scope = { 6 | "list", 7 | "tuple", 8 | "dictionary", 9 | "set", 10 | "parenthesized_expression", 11 | "generator_expression", 12 | "list_comprehension", 13 | "set_comprehension", 14 | "dictionary_comprehension", 15 | "tuple_pattern", 16 | "list_pattern", 17 | "argument_list", 18 | "parameters", 19 | }, 20 | scope_open = { 21 | "assignment", 22 | "import_from_statement", 23 | "return_statement", 24 | "expression_list", 25 | "boolean_operator", 26 | "binary_operator", 27 | }, 28 | scope_open_extended = { 29 | "if_statement", 30 | "elif_clause", 31 | "else_clause", 32 | "for_statement", 33 | "match_statement", 34 | "case_clause", 35 | "while_statement", 36 | "with_statement", 37 | "try_statement", 38 | "except_clause", 39 | "finally_clause", 40 | "class_definition", 41 | "function_definition", 42 | "lambda", 43 | }, 44 | indent_align = { 45 | "argument_list", 46 | "parameters", 47 | "list", 48 | "tuple", 49 | }, 50 | indent_list = { 51 | "argument_list", 52 | "parameters", 53 | "list", 54 | "tuple", 55 | }, 56 | dedent_child = { 57 | if_statement = { "else_clause", "elif_clause", "parenthesized_expression" }, 58 | elif_clause = { "parenthesized_expression" }, 59 | while_statement = { "else_clause", "parenthesized_expression" }, 60 | try_statement = { "except_clause", "else_clause", "finally_clause", "parenthesized_expression" }, 61 | }, 62 | ignore = { 63 | "block", 64 | }, 65 | handlers = { 66 | on_initial = { 67 | ch.multiline_string_literal("string"), 68 | ch.multiline_string_literal("string_content"), 69 | }, 70 | on_traverse = { 71 | ch.dedent_pattern("else", "identifier", "if_statement"), 72 | ch.dedent_pattern("elif", "identifier", "if_statement"), 73 | ch.dedent_pattern("except", "identifier", "try_statement"), 74 | ch.dedent_pattern("finnally", "identifier", "try_statement"), 75 | }, 76 | }, 77 | } 78 | 79 | return config 80 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/rust.lua: -------------------------------------------------------------------------------- 1 | local ch = require("nvim-yati.handlers.common") 2 | local handlers = require("nvim-yati.handlers.rust") 3 | 4 | ---@type YatiBuiltinConfig 5 | local config = { 6 | scope = { 7 | "mod_item", 8 | "enum_variant_list", 9 | "ordered_field_declaration_list", 10 | "field_declaration_list", 11 | "field_initializer_list", 12 | "declaration_list", 13 | "function_item", 14 | "parameters", 15 | "struct_expression", 16 | "match_block", 17 | "tuple_expression", 18 | "array_expression", 19 | "match_arm", 20 | "match_block", 21 | "if_expression", 22 | "else_clause", 23 | "if_let_expression", 24 | "while_expression", 25 | "for_expression", 26 | "loop_expression", 27 | "assignment_expression", 28 | "arguments", 29 | "parameters", 30 | "type_parameters", 31 | "type_arguments", 32 | "block", 33 | "use_list", 34 | "macro_definition", 35 | "token_tree", 36 | "parenthesized_expression", 37 | }, 38 | scope_open = { 39 | "const_item", 40 | "let_declaration", 41 | "assignment_expression", 42 | "binary_expression", 43 | "compound_assignment_expr", 44 | "field_expression", 45 | "call_expression", 46 | "where_clause", 47 | "await_expression", 48 | }, 49 | dedent_child = { 50 | if_expression = { "block", "else_clause" }, 51 | if_let_expression = { "block", "else_clause" }, 52 | else_clause = { "block" }, 53 | while_expression = { "block" }, 54 | for_expression = { "block" }, 55 | loop_expression = { "block" }, 56 | function_item = { "parameters", "where_clause", "type_parameters" }, 57 | }, 58 | ignore = { 59 | "string_content", 60 | }, 61 | handlers = { 62 | on_initial = { 63 | ch.multiline_string_literal("string_literal"), 64 | ch.multiline_string_literal("raw_string_literal"), 65 | ch.block_comment_extra_indent("block_comment", { "'*/'" }), 66 | handlers.dedent_field_on_close_initial("field_expression"), 67 | handlers.dedent_field_on_close_initial("await_expression"), 68 | }, 69 | on_traverse = { 70 | ch.chained_field_call("arguments", "field_expression", "field"), 71 | handlers.dedent_field_on_close_traverse("field_expression", "field_identifier"), 72 | handlers.dedent_field_on_close_traverse("await_expression", "'await'"), 73 | }, 74 | }, 75 | } 76 | 77 | return config 78 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/styled.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.configs.css") 2 | local extend = require("nvim-yati.config").extend 3 | 4 | return extend(config, {}) 5 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/toml.lua: -------------------------------------------------------------------------------- 1 | ---@type YatiBuiltinConfig 2 | local config = { 3 | scope = { 4 | "array", 5 | }, 6 | } 7 | 8 | return config 9 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/tsx.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.configs.typescript") 2 | local extend = require("nvim-yati.config").extend 3 | 4 | return extend(config, {}) 5 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/typescript.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-yati.configs.javascript") 2 | local extend = require("nvim-yati.config").extend 3 | 4 | return extend(config, { 5 | scope = { 6 | "object_type", 7 | "tuple_type", 8 | "enum_body", 9 | "type_arguments", 10 | "type_parameters", 11 | "implements_clause", 12 | "interface_body", 13 | }, 14 | scope_open = { 15 | "property_signature", 16 | "conditional_type", 17 | "required_parameter", 18 | "property_signature", 19 | "type_annotation", 20 | "type_alias_declaration", 21 | }, 22 | ignore = { "union_type" }, 23 | dedent_child = { 24 | ["type_alias_declaration"] = { "object_type" }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /lua/nvim-yati/configs/vue.lua: -------------------------------------------------------------------------------- 1 | ---@type YatiBuiltinConfig 2 | local config = { 3 | scope = { 4 | "template_element", 5 | "element", 6 | "start_tag", 7 | "end_tag", 8 | "interpolation", 9 | "self_closing_tag", 10 | }, 11 | ignore = { "text", "raw_text" }, 12 | } 13 | 14 | return config 15 | -------------------------------------------------------------------------------- /lua/nvim-yati/context.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-yati.utils") 2 | 3 | local function always_true() 4 | return true 5 | end 6 | 7 | local function create_cross_tree_stack(node, parser, filter) 8 | local sr, sc, er, ec = node:range() 9 | 10 | local trees = {} 11 | parser:for_each_tree(function(tree, lang_tree) 12 | local root = tree:root() 13 | local rsr, rsc, rer, rec = root:range() 14 | if not utils.range_contains(rsr, rsc, rer, rec, sr, sc, er, ec) then 15 | return 16 | end 17 | local min_capture_node = root:descendant_for_range(sr, sc, er, ec) 18 | 19 | while min_capture_node and not filter(min_capture_node, lang_tree:lang()) do 20 | min_capture_node = min_capture_node:parent() 21 | end 22 | if min_capture_node and utils.node_contains(min_capture_node, node) then 23 | trees[#trees + 1] = { 24 | lang = lang_tree:lang(), 25 | tstree = tree, 26 | min_capture_node = min_capture_node, 27 | } 28 | end 29 | end) 30 | 31 | table.sort(trees, function(a, b) 32 | local is_same = utils.node_contains(a.tstree:root(), b.tstree:root()) 33 | and utils.node_contains(b.tstree:root(), a.tstree:root()) 34 | if is_same then 35 | return utils.node_contains(a.min_capture_node, b.min_capture_node) 36 | end 37 | return utils.node_contains(a.tstree:root(), b.tstree:root()) 38 | end) 39 | 40 | return trees 41 | end 42 | 43 | ---@class YatiContext 44 | ---@field node userdata 45 | ---@field bufnr integer 46 | ---@field lnum integer 47 | ---@field computed_indent integer 48 | ---@field shift integer 49 | ---@field stage "initial"|"traverse" 50 | ---@field tree_stack { tstree: userdata, lang: string, min_capture_node: userdata }[] 51 | ---@field parser LanguageTree 52 | ---@field filter fun(node: userdata, lang: string|nil):boolean 53 | ---@field cget fun(lang: string):YatiLangConfig|nil 54 | ---@field has_fallback boolean 55 | local Context = {} 56 | 57 | ---@param lnum integer 58 | ---@param bufnr integer 59 | ---@param filter fun(node: userdata):boolean 60 | ---@param cget fun(lang: string):YatiLangConfig|nil 61 | ---@return YatiContext|nil 62 | function Context:new(lnum, bufnr, filter, cget) 63 | local obj = { 64 | lnum = lnum, 65 | stage = "initial", 66 | bufnr = bufnr, 67 | shift = utils.get_shift(bufnr), 68 | filter = filter or always_true, 69 | cget = cget, 70 | computed_indent = 0, 71 | tree_stack = {}, 72 | } 73 | 74 | setmetatable(obj, { __index = self }) 75 | 76 | local parser = utils.get_parser(bufnr) 77 | local node = utils.get_node_at_line(lnum, false, bufnr, filter) 78 | if not node then 79 | return 80 | end 81 | 82 | obj.node = node 83 | obj.parser = parser 84 | obj.tree_stack = create_cross_tree_stack(node, parser, filter) 85 | 86 | return obj 87 | end 88 | 89 | ---@param self YatiContext 90 | local function _peek_parent(self) 91 | if not self.node then 92 | return 93 | end 94 | local cur = self.node:parent() 95 | local stack_pos = #self.tree_stack - 1 96 | -- we need to check whether the new parent contains old node 97 | -- because `min_capture_node` is not always correct (multiple 98 | -- trees span same range) 99 | while 100 | not cur 101 | or not self.filter(cur, self.tree_stack[stack_pos + 1].lang) 102 | or not utils.node_contains(cur, self.node) 103 | do 104 | if cur then 105 | cur = cur:parent() 106 | elseif stack_pos >= 1 then 107 | cur = self.tree_stack[stack_pos].min_capture_node 108 | stack_pos = stack_pos - 1 109 | else 110 | return 111 | end 112 | end 113 | return cur, stack_pos + 1 114 | end 115 | 116 | ---@return string|nil 117 | function Context:lang() 118 | local entry = self.tree_stack[#self.tree_stack] 119 | if entry then 120 | return entry.lang 121 | end 122 | end 123 | 124 | ---@return string|nil 125 | function Context:parent_lang() 126 | local _, pos = _peek_parent(self) 127 | if pos ~= nil and pos >= 1 then 128 | return self.tree_stack[pos].lang 129 | end 130 | end 131 | 132 | ---@return YatiLangConfig|nil 133 | function Context:conf() 134 | local lang = self:lang() 135 | if lang then 136 | return self.cget(lang) 137 | end 138 | end 139 | 140 | ---@return YatiLangConfig|nil 141 | function Context:p_conf() 142 | local lang = self:parent_lang() 143 | if lang then 144 | return self.cget(lang) 145 | end 146 | end 147 | 148 | ---@return YatiNodesConfig|nil 149 | function Context:nodes_conf() 150 | local conf = self:conf() 151 | if conf then 152 | return conf.nodes 153 | end 154 | end 155 | 156 | ---@return YatiNodesConfig|nil 157 | function Context:p_nodes_conf() 158 | local conf = self:p_conf() 159 | if conf then 160 | return conf.nodes 161 | end 162 | end 163 | 164 | ---@return YatiHandler[] 165 | function Context:handlers() 166 | local conf = self:conf() 167 | if conf then 168 | local handlers = conf.handlers 169 | if self.stage == "initial" then 170 | return handlers.on_initial 171 | else 172 | return handlers.on_traverse 173 | end 174 | end 175 | return {} 176 | end 177 | 178 | ---@return YatiHandler[] 179 | function Context:p_handlers() 180 | local conf = self:p_conf() 181 | if conf then 182 | local handlers = conf.handlers 183 | if self.stage == "initial" then 184 | return handlers.on_initial 185 | else 186 | return handlers.on_traverse 187 | end 188 | end 189 | return {} 190 | end 191 | 192 | ---@return userdata|nil 193 | function Context:parent() 194 | local node = _peek_parent(self) 195 | return node 196 | end 197 | 198 | ---@return userdata|nil 199 | function Context:prev_sibling() 200 | if not self.node then 201 | return 202 | end 203 | local cur = self.node:prev_sibling() 204 | while cur and not self.filter(cur, self:lang()) do 205 | cur = cur:prev_sibling() 206 | end 207 | return cur 208 | end 209 | 210 | ---@return userdata|nil 211 | function Context:next_sibling() 212 | if not self.node then 213 | return 214 | end 215 | local cur = self.node:next_sibling() 216 | while cur and not self.filter(cur, self:lang()) do 217 | cur = cur:next_sibling() 218 | end 219 | return cur 220 | end 221 | 222 | ---@return userdata|nil 223 | function Context:first_sibling() 224 | local parent = self:parent() 225 | if parent then 226 | for node in parent:iter_children() do 227 | if self.filter(node) then 228 | return node 229 | end 230 | end 231 | end 232 | end 233 | 234 | ---@return userdata|nil 235 | function Context:last_sibling() 236 | local parent = self:parent() 237 | if parent then 238 | local res 239 | for node in parent:iter_children() do 240 | if self.filter(node, self:lang()) then 241 | res = node 242 | end 243 | end 244 | if res then 245 | return res 246 | end 247 | end 248 | end 249 | 250 | ---@return userdata|nil 251 | function Context:to_parent() 252 | if not self.node then 253 | return 254 | end 255 | local cur = self.node:parent() 256 | while 257 | not cur 258 | or not self.filter(cur, self.tree_stack[#self.tree_stack].lang) 259 | or not utils.node_contains(cur, self.node) 260 | do 261 | if cur then 262 | cur = cur:parent() 263 | elseif #self.tree_stack > 1 then 264 | table.remove(self.tree_stack) 265 | cur = self.tree_stack[#self.tree_stack].min_capture_node 266 | else 267 | break 268 | end 269 | end 270 | self.node = cur 271 | 272 | return self.node 273 | end 274 | 275 | ---@param indent_delta integer 276 | function Context:add(indent_delta) 277 | self.computed_indent = self.computed_indent + indent_delta 278 | end 279 | 280 | ---@param indent integer 281 | function Context:set(indent) 282 | self.computed_indent = indent 283 | end 284 | 285 | ---@param new_node userdata 286 | function Context:relocate(new_node, follow_parent) 287 | if new_node ~= self.node then 288 | if follow_parent then 289 | while self.node and not utils.node_contains(self.node, new_node) do 290 | self:to_parent() 291 | end 292 | else 293 | self.node = new_node 294 | self.tree_stack = create_cross_tree_stack(new_node, self.parser, self.filter) 295 | end 296 | end 297 | return true 298 | end 299 | 300 | function Context:begin_traverse() 301 | self.stage = "traverse" 302 | end 303 | 304 | function Context:parse() 305 | if not self.parser:is_valid() then 306 | self.parser:parse() 307 | end 308 | end 309 | 310 | function Context:fallback() 311 | self.has_fallback = true 312 | return false -- not continue 313 | end 314 | 315 | return Context 316 | -------------------------------------------------------------------------------- /lua/nvim-yati/fallback.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-yati.utils") 2 | 3 | local M = {} 4 | 5 | ---@alias YatiFallbackFn fun(lnum: integer, computed: integer, bufnr: integer): integer 6 | ---@alias YatiFallback "cindent"|"asis"|"auto"|YatiFallbackFn 7 | 8 | ---@param lnum integer 9 | ---@param computed integer 10 | ---@param bufnr integer 11 | ---@return integer 12 | function M.vim_cindent(lnum, computed, bufnr) 13 | -- TODO: lispindent ? 14 | local cindent = vim.api.nvim_buf_call(bufnr, function() 15 | return vim.fn.cindent(lnum + 1) 16 | end) 17 | return cindent + computed 18 | end 19 | 20 | ---@param lnum integer 21 | ---@param computed integer 22 | ---@param bufnr integer 23 | ---@return integer 24 | function M.as_is(lnum, computed, bufnr) 25 | return utils.cur_indent(lnum, bufnr) + computed 26 | end 27 | 28 | function M.vim_auto() 29 | return -1 30 | end 31 | 32 | ---Get resolved fallback method from config option 33 | ---@param fallback YatiFallback 34 | function M.get_fallback(fallback) 35 | if type(fallback) == "function" then 36 | return fallback 37 | elseif fallback == "cindent" then 38 | return M.vim_cindent 39 | elseif fallback == "asis" then 40 | return M.as_is 41 | else 42 | return M.vim_auto 43 | end 44 | end 45 | 46 | return M 47 | -------------------------------------------------------------------------------- /lua/nvim-yati/handlers/common.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-yati.utils") 2 | local logger = require("nvim-yati.logger") 3 | local nt = utils.node_type 4 | 5 | local M = {} 6 | 7 | function M.block_comment_extra_indent(comment, ignores, pattern) 8 | pattern = pattern or "^%s*%*" 9 | ---@param ctx YatiContext 10 | return function(ctx) 11 | if utils.get_buf_line(ctx.bufnr, ctx.lnum):match(pattern) == nil then 12 | return 13 | end 14 | ctx:parse() 15 | 16 | -- NOTE: this mutates cursor to skip comment initially 17 | while ctx.node and vim.tbl_contains(ignores, nt(ctx.node)) do 18 | logger("handler", "Skip initial comment " .. nt(ctx.node)) 19 | ctx:to_parent() 20 | end 21 | 22 | local node = ctx.node 23 | if node and node:type() == comment and node:start() ~= ctx.lnum then 24 | logger("handler", string.format("Match inner block comment (%s), add extra indent", nt(ctx.node))) 25 | ctx:add(1) 26 | return true 27 | end 28 | end 29 | end 30 | 31 | function M.ternary_flatten_indent(ternary) 32 | ---@param ctx YatiContext 33 | return function(ctx) 34 | local node = ctx.node 35 | local parent = ctx:parent() 36 | local prev = ctx:prev_sibling() 37 | 38 | if parent and parent:type() == ternary then 39 | ctx:to_parent() 40 | if parent and parent:parent():type() == ternary and parent:child(0) == node then 41 | prev = ctx:prev_sibling() 42 | end 43 | 44 | while ctx:parent():type() == ternary do 45 | ctx:to_parent() 46 | end 47 | 48 | if node:type() == "?" or node:type() == ":" then 49 | ctx:add(ctx.shift) 50 | elseif prev and (prev:type() == "?" or prev:type() == ":") then 51 | -- ternary.js #L39 52 | if prev:start() == node:start() and utils.is_first_node_on_line(prev, ctx.bufnr) then 53 | ctx:add(ctx.shift * 2) 54 | elseif ctx.node:start() ~= node:start() then 55 | ctx:add(ctx.shift) 56 | end 57 | end 58 | 59 | return true 60 | end 61 | end 62 | end 63 | 64 | ---Fix indent in arguemnt of chained function calls (chained_call.js) 65 | function M.chained_field_call(arguemnts, field_expr, field_name) 66 | ---@param ctx YatiContext 67 | return function(ctx) 68 | local node = ctx.node 69 | local sibling = ctx:prev_sibling() 70 | local field = sibling and sibling:field(field_name)[1] 71 | if 72 | node 73 | and sibling 74 | and field 75 | and node:type() == arguemnts 76 | and sibling:type() == field_expr 77 | and sibling:start() ~= sibling:end_() 78 | then 79 | ctx:relocate(field) 80 | return true 81 | end 82 | end 83 | end 84 | 85 | function M.multiline_string_literal(str) 86 | ---@param ctx YatiContext 87 | return function(ctx) 88 | if ctx.node:type() == str and ctx.node:start() ~= ctx.lnum then 89 | if utils.is_line_empty(ctx.lnum, ctx.bufnr) then 90 | return ctx:fallback() 91 | else 92 | -- TODO: replace with fallback 93 | ctx:set(utils.cur_indent(ctx.lnum, ctx.bufnr)) 94 | end 95 | return false 96 | end 97 | end 98 | end 99 | 100 | function M.multiline_string_injection(str, close_delim, should_indent) 101 | if should_indent == nil then 102 | should_indent = true 103 | end 104 | ---@param ctx YatiContext 105 | return function(ctx) 106 | local parent = ctx:parent() 107 | if parent and parent:type() == str then 108 | -- in injection 109 | if ctx:lang() ~= ctx:parent_lang() and ctx.node:start() ~= parent:start() then 110 | if should_indent then 111 | ctx:add(ctx.shift) 112 | end 113 | elseif ctx.node:type() ~= close_delim then 114 | ctx:add(utils.cur_indent(ctx.node:start(), ctx.bufnr)) 115 | return false 116 | end 117 | return true 118 | end 119 | end 120 | end 121 | 122 | function M.dedent_pattern(pattern, node_type, indent_node_type) 123 | ---@param ctx YatiContext 124 | return function(ctx) 125 | local node = ctx.node 126 | local line = utils.get_buf_line(ctx.bufnr, node:start()) 127 | if not line then 128 | return 129 | end 130 | line = vim.trim(line) 131 | if node:type() == node_type and line:match(pattern) ~= nil then 132 | local next = utils.try_find_parent(node, function(parent) 133 | return parent:type() == indent_node_type 134 | end) 135 | if next then 136 | ctx:relocate(next, true) 137 | end 138 | end 139 | end 140 | end 141 | 142 | return M 143 | -------------------------------------------------------------------------------- /lua/nvim-yati/handlers/default.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-yati.utils") 2 | 3 | local M = {} 4 | 5 | local nt = utils.node_type 6 | 7 | ---@param ctx YatiContext 8 | ---@param parent userdata 9 | local function handle_indent_align(ctx, parent) 10 | local first_no_delim_sib = parent:child(1) 11 | if 12 | first_no_delim_sib 13 | and first_no_delim_sib:start() == first_no_delim_sib:end_() 14 | and first_no_delim_sib:start() == parent:start() 15 | then 16 | local scol = utils.get_first_nonblank_col_at_line(parent:start(), ctx.bufnr) 17 | local _, ecol = first_no_delim_sib:start() 18 | ctx:add(ecol - scol) 19 | 20 | -- navigate up to skip same line node 21 | while ctx:parent() and ctx:parent():start() == parent:start() do 22 | ctx:to_parent() 23 | end 24 | 25 | return parent 26 | end 27 | end 28 | 29 | ---@param ctx YatiContext 30 | function M.on_initial(ctx) 31 | local node = ctx.node 32 | 33 | -- The line is empty 34 | if not node or node:start() ~= ctx.lnum then 35 | local prev_node 36 | local cur_line = utils.prev_nonblank_lnum(ctx.lnum, ctx.bufnr) 37 | prev_node = utils.get_node_at_line(cur_line, false, ctx.bufnr, ctx.filter) 38 | 39 | --[[ 40 | -- Try find node considered always 'open' for last indent 41 | -- Example: 42 | -- if true: 43 | -- some() 44 | -- 45 | -- | 46 | --]] 47 | while prev_node and ctx:nodes_conf()[nt(prev_node)].indent_zero do 48 | cur_line = utils.prev_nonblank_lnum(cur_line, ctx.bufnr) 49 | if cur_line < node:start() then 50 | prev_node = nil 51 | break 52 | end 53 | prev_node = utils.get_node_at_line(cur_line, false, ctx.bufnr, ctx.filter) 54 | end 55 | prev_node = utils.try_find_parent(prev_node, function(parent) 56 | return ctx:nodes_conf()[nt(parent)].scope_open_extended 57 | end) 58 | 59 | -- If prev_node is contained inside, then we use prev_node as indent base 60 | if prev_node and utils.node_contains(node, prev_node) then 61 | node = prev_node 62 | ctx:relocate(node) 63 | end 64 | 65 | while node and ctx:nodes_conf()[nt(node)].ignore do 66 | node = ctx:to_parent() 67 | end 68 | 69 | if not node then 70 | return ctx:fallback() 71 | end 72 | 73 | local attrs = ctx:nodes_conf()[nt(node)] 74 | if attrs.indent_fallback or node:has_error() then 75 | return ctx:fallback() 76 | end 77 | 78 | -- If the node is not at the same line and it's an indent node, we should indent 79 | if node:start() ~= ctx.lnum and attrs.scope and (attrs.scope_open_extended or node:end_() >= ctx.lnum) then 80 | local aligned = attrs.indent_align and handle_indent_align(ctx, node) 81 | if not aligned then 82 | ctx:add(ctx.shift) 83 | end 84 | end 85 | end 86 | 87 | return true 88 | end 89 | 90 | ---@param ctx YatiContext 91 | local function check_indent_range(ctx) 92 | local node = ctx.node 93 | local parent = ctx:parent() 94 | if not parent then 95 | return false 96 | end 97 | 98 | local attrs = ctx:nodes_conf()[nt(parent)] 99 | 100 | -- special case: not direct parent 101 | if node:parent() ~= parent then 102 | return ctx.node:start() ~= parent:start() 103 | end 104 | 105 | -- only expand range if more than one child 106 | -- see arrow_func_in_args.js 107 | -- but if the node is aligned indent, we still need to check it 108 | -- see 109 | if attrs.indent_list and (parent:named_child_count() > 1 or attrs.indent_align) then 110 | local srow = node:start() 111 | local erow = node:end_() 112 | 113 | local prev = node:prev_sibling() 114 | while prev and prev:end_() == srow do 115 | srow = prev:start(0) 116 | prev = prev:prev_sibling() 117 | end 118 | 119 | local next = node:next_sibling() 120 | while next and next:start() == erow do 121 | erow = next:end_() 122 | next = next:next_sibling() 123 | end 124 | 125 | return srow ~= parent:start() or erow ~= parent:end_() 126 | else 127 | return ctx.node:start() ~= ctx:first_sibling():end_() 128 | end 129 | end 130 | 131 | ---@param ctx YatiContext 132 | function M.on_traverse(ctx) 133 | local node = ctx.node 134 | local parent = ctx:parent() 135 | local conf = ctx:nodes_conf() 136 | if not conf then 137 | return ctx:fallback() 138 | end 139 | 140 | if conf[nt(node)].indent_zero then 141 | ctx:set(0) 142 | return false 143 | end 144 | 145 | local attrs = conf[nt(node)] 146 | if attrs.indent_fallback then 147 | return ctx:fallback() 148 | end 149 | 150 | if parent then 151 | local p_attrs = ctx:p_nodes_conf()[nt(parent)] 152 | if p_attrs.indent_fallback then 153 | return ctx:fallback() 154 | end 155 | 156 | local should_indent = p_attrs.scope and check_indent_range(ctx) 157 | local should_indent_align = should_indent and p_attrs.indent_align 158 | 159 | -- TODO: deal with no direct parent 160 | if parent == node:parent() then 161 | should_indent = should_indent 162 | and ctx:prev_sibling() ~= nil 163 | and (not vim.tbl_contains(p_attrs.dedent_child, nt(node))) 164 | should_indent_align = should_indent and p_attrs.indent_align 165 | 166 | -- Do not consider close delimiter for aligned indent 167 | should_indent = should_indent and (ctx:next_sibling() ~= nil or p_attrs.scope_open) 168 | end 169 | 170 | local aligned = should_indent_align and handle_indent_align(ctx, parent) 171 | 172 | if should_indent and not aligned then 173 | ctx:add(ctx.shift) 174 | end 175 | end 176 | 177 | return true 178 | end 179 | 180 | return M 181 | -------------------------------------------------------------------------------- /lua/nvim-yati/handlers/init.lua: -------------------------------------------------------------------------------- 1 | ---@alias YatiHandler fun(ctx: YatiContext):boolean 2 | 3 | ---@class YatiHandlers 4 | ---@field on_initial YatiHandlers[] 5 | ---@field on_traverse YatiHandlers[] 6 | 7 | local default_handlers = require("nvim-yati.handlers.default") 8 | 9 | local M = {} 10 | 11 | ---@param ctx YatiContext 12 | function M.handle_initial(ctx) 13 | for _, handler in ipairs(ctx:handlers() or {}) do 14 | local should_cont = handler(ctx) 15 | if should_cont ~= nil then 16 | return should_cont 17 | end 18 | end 19 | return default_handlers.on_initial(ctx) 20 | end 21 | 22 | ---@param ctx YatiContext 23 | function M.handle_traverse(ctx) 24 | for _, handlers in ipairs({ ctx:handlers(), ctx:p_handlers() }) do 25 | for _, handler in ipairs(handlers or {}) do 26 | local should_cont = handler(ctx) 27 | if should_cont ~= nil then 28 | return should_cont 29 | end 30 | end 31 | end 32 | return default_handlers.on_traverse(ctx) 33 | end 34 | 35 | return M 36 | -------------------------------------------------------------------------------- /lua/nvim-yati/handlers/rust.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-yati.utils") 2 | local nt = utils.node_type 3 | 4 | local M = {} 5 | 6 | local function check_prev_field_closed(field_node, bufnr) 7 | local lines = vim.split(vim.treesitter.get_node_text(field_node, bufnr, {}), "\n") 8 | for i = #lines, 1, -1 do 9 | local first_char = vim.trim(lines[i]):sub(1, 1) 10 | -- skip previous chained field or empty line 11 | if first_char ~= "" and first_char ~= "." then 12 | -- find close delimeter 13 | if first_char:find("^[%]})>]") ~= nil then 14 | local prev_line = field_node:end_() - #lines + i 15 | return utils.get_node_at_line(prev_line, false, bufnr) 16 | else 17 | break 18 | end 19 | end 20 | end 21 | end 22 | 23 | -- Related: sample.rs#L203 24 | function M.dedent_field_on_close_initial(field_expression) 25 | ---@param ctx YatiContext 26 | return function(ctx) 27 | if ctx.node and nt(ctx.node) == field_expression and ctx.node:child(0) then 28 | local prev_close_node = check_prev_field_closed(ctx.node:child(0), ctx.bufnr) 29 | if prev_close_node then 30 | ctx:relocate(prev_close_node) 31 | return true 32 | end 33 | end 34 | end 35 | end 36 | 37 | function M.dedent_field_on_close_traverse(field_expression, field_type) 38 | ---@param ctx YatiContext 39 | return function(ctx) 40 | if 41 | (nt(ctx.node) == field_type or ctx.node:type() == ".") 42 | and ctx:parent() 43 | and nt(ctx:parent()) == field_expression 44 | then 45 | local prev_close_node = check_prev_field_closed(ctx:first_sibling(), ctx.bufnr) 46 | if prev_close_node then 47 | ctx:relocate(prev_close_node) 48 | return true 49 | end 50 | end 51 | end 52 | end 53 | 54 | return M 55 | -------------------------------------------------------------------------------- /lua/nvim-yati/indent.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-yati.utils") 2 | local o = require("nvim-yati.config") 3 | local handlers = require("nvim-yati.handlers") 4 | local Context = require("nvim-yati.context") 5 | local logger = require("nvim-yati.logger") 6 | local get_fallback = require("nvim-yati.fallback").get_fallback 7 | local nt = utils.node_type 8 | 9 | local M = {} 10 | 11 | ---@param ctx YatiContext 12 | local function check_lazy_exit(ctx) 13 | local conf = ctx:conf() 14 | if 15 | conf 16 | and conf.lazy 17 | and ctx.node 18 | and ctx.node:start() ~= ctx.lnum 19 | and utils.is_first_node_on_line(ctx.node, ctx.bufnr) 20 | then 21 | ctx:add(utils.cur_indent(ctx.node:start(), ctx.bufnr)) 22 | logger("main", "Exit early for lazy mode at " .. nt(ctx.node)) 23 | return true 24 | end 25 | end 26 | 27 | ---@param conf YatiLangConfig 28 | ---@param lnum integer 29 | ---@param computed integer 30 | ---@param bufnr integer 31 | ---@return integer 32 | local function exec_fallback(conf, lnum, computed, bufnr) 33 | return get_fallback(conf.fallback)(lnum, computed, bufnr) 34 | end 35 | 36 | function M.get_indent(lnum, bufnr, user_conf) 37 | bufnr = bufnr or vim.api.nvim_get_current_buf() 38 | 39 | user_conf = user_conf or o.get_user_config() 40 | local cget = o.with_user_config_get(user_conf) 41 | 42 | local root_tree = utils.get_parser(bufnr) 43 | 44 | if not root_tree then 45 | return -1 46 | end 47 | 48 | -- Firstly, ensure the tree is updated 49 | if not root_tree:is_valid() then 50 | root_tree:parse() 51 | end 52 | 53 | local bootstrap_lang = utils.get_lang_at_line(lnum, bufnr) 54 | if not bootstrap_lang then 55 | return -1 56 | end 57 | 58 | local bootstrap_conf = cget(bootstrap_lang) 59 | if not bootstrap_conf then 60 | return -1 61 | end 62 | 63 | local node_filter = function(node, lang) 64 | local c = (lang and cget(lang)) or bootstrap_conf 65 | return not c.nodes[nt(node)].ignore 66 | end 67 | local ctx = Context:new(lnum, bufnr, node_filter, cget) 68 | if not ctx then 69 | return -1 70 | end 71 | 72 | logger("main", string.format("Bootstrap node %s(%s)", nt(ctx.node), ctx:lang())) 73 | 74 | local should_cont = handlers.handle_initial(ctx) 75 | if ctx.has_fallback then 76 | local conf = ctx:conf() or bootstrap_conf 77 | return exec_fallback(conf, lnum, 0, bufnr) 78 | elseif not ctx.node or not should_cont then 79 | return ctx.computed_indent 80 | end 81 | 82 | logger("main", string.format("Initial node %s(%s), computed %s", nt(ctx.node), ctx:lang(), ctx.computed_indent)) 83 | 84 | if check_lazy_exit(ctx) then 85 | return ctx.computed_indent 86 | end 87 | 88 | ctx:begin_traverse() 89 | 90 | -- main traverse loop 91 | while ctx.node do 92 | local prev_node = ctx.node 93 | local prev_lang = ctx:lang() 94 | 95 | should_cont = handlers.handle_traverse(ctx) 96 | if ctx.has_fallback then 97 | local lang = ctx:lang() 98 | local node = ctx.node 99 | if lang and node and ctx:conf() then 100 | return exec_fallback(ctx:conf(), node:start(), ctx.computed_indent, bufnr) 101 | else 102 | return exec_fallback(bootstrap_conf, lnum, 0, bufnr) 103 | end 104 | elseif not should_cont then 105 | break 106 | end 107 | 108 | -- force traversing up if not changed in handlers 109 | if prev_node == ctx.node then 110 | ctx:to_parent() 111 | end 112 | 113 | if ctx.node then 114 | logger( 115 | "main", 116 | string.format( 117 | "Traverse from %s(%s) to %s(%s), computed %s", 118 | nt(prev_node), 119 | prev_lang, 120 | nt(ctx.node), 121 | ctx:lang(), 122 | ctx.computed_indent 123 | ) 124 | ) 125 | end 126 | 127 | if check_lazy_exit(ctx) then 128 | break 129 | end 130 | end 131 | 132 | return ctx.computed_indent 133 | end 134 | 135 | function M.indentexpr(vlnum) 136 | if vlnum == nil then 137 | vlnum = vim.v.lnum 138 | end 139 | 140 | logger("START", "Line " .. vlnum) 141 | local ok, indent = xpcall(M.get_indent, debug.traceback, vlnum - 1) 142 | if ok then 143 | logger("END", "Total computed: " .. indent) 144 | return indent 145 | else 146 | logger("END", "Error: " .. indent) 147 | -- only show err if option explicitly set to false 148 | if o.get_user_config().suppress_indent_err == false then 149 | vim.schedule(function() 150 | vim.notify_once( 151 | string.format( 152 | "[nvim-yati]: indent computation for line %s failed, consider submitting an issue for it\n%s", 153 | vlnum, 154 | indent 155 | ), 156 | vim.log.levels.WARN 157 | ) 158 | end) 159 | end 160 | return -1 161 | end 162 | end 163 | 164 | return M 165 | -------------------------------------------------------------------------------- /lua/nvim-yati/internal.lua: -------------------------------------------------------------------------------- 1 | local o = require("nvim-yati.config") 2 | local is_mod_enabled = require("nvim-treesitter.configs").is_enabled 3 | 4 | local M = {} 5 | local stored_expr = {} 6 | 7 | function M.attach(bufnr, lang) 8 | if is_mod_enabled("indent", lang, bufnr) then 9 | if not o.get_user_config().suppress_conflict_warning then 10 | vim.notify_once( 11 | string.format( 12 | '[nvim-yati] is disabled. The builtin indent module has been enabled, add "%s" to the disabled language of indent module if you want to use nvim-yati instead. Otherwise, disable "%s" for nvim-yati to suppress the message.', 13 | lang, 14 | lang 15 | ), 16 | vim.log.levels.INFO, 17 | { title = "[nvim-yati]: Disabled" } 18 | ) 19 | end 20 | return 21 | end 22 | stored_expr[bufnr] = vim.bo[bufnr].indentexpr 23 | vim.bo[bufnr].indentexpr = "v:lua.require'nvim-yati.indent'.indentexpr()" 24 | end 25 | 26 | function M.detach(bufnr) 27 | vim.bo[bufnr].indentexpr = stored_expr[bufnr] 28 | stored_expr[bufnr] = nil 29 | end 30 | 31 | return M 32 | -------------------------------------------------------------------------------- /lua/nvim-yati/logger.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | enable = vim.env.DEBUG_YATI, 3 | disabled_context = {}, 4 | } 5 | 6 | setmetatable(M, { 7 | __call = function(_, context, msg) 8 | if M.enable and not M.disabled_context[context] then 9 | print(string.format("[nvim-yati][%s]: ", context) .. msg) 10 | end 11 | end, 12 | }) 13 | 14 | function M.toggle() 15 | M.enable = not M.enable 16 | end 17 | 18 | function M.disable(context) 19 | M.disabled_context[context] = true 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /lua/nvim-yati/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@return LanguageTree 4 | function M.get_parser(bufnr) 5 | local ft = vim.bo[bufnr].filetype 6 | local lang = vim.treesitter.language.get_lang(ft) 7 | return vim.treesitter.get_parser(bufnr, lang) 8 | end 9 | 10 | -- `get_lang` is only available in Neovim v0.9 and above 11 | if vim.treesitter.language.get_lang == nil then 12 | M.get_parser = require("nvim-treesitter.parser").get_parser 13 | end 14 | 15 | ---@return string 16 | function M.get_buf_line(bufnr, lnum) 17 | return vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1] or "" 18 | end 19 | 20 | function M.get_shift(bufnr) 21 | -- NOTE: Not work with 'vartabstop' 22 | local shift = vim.bo[bufnr].shiftwidth 23 | if shift <= 0 then 24 | shift = vim.bo[bufnr].tabstop 25 | end 26 | return shift 27 | end 28 | 29 | function M.node_type(node) 30 | if node:named() then 31 | return node:type() 32 | else 33 | return "'" .. node:type() .. "'" 34 | end 35 | end 36 | 37 | function M.cur_indent(lnum, bufnr) 38 | bufnr = bufnr or vim.api.nvim_get_current_buf() 39 | return vim.api.nvim_buf_call(bufnr, function() 40 | return vim.fn.indent(lnum + 1) 41 | end) 42 | end 43 | 44 | function M.indent_diff(l1, l2, bufnr) 45 | return M.cur_indent(l1, bufnr) - M.cur_indent(l2, bufnr) 46 | end 47 | 48 | function M.prev_nonblank_lnum(lnum, bufnr) 49 | bufnr = bufnr or vim.api.nvim_get_current_buf() 50 | local prev = lnum - 1 51 | while prev >= 0 do 52 | local line = M.get_buf_line(bufnr, prev) 53 | if string.match(line, "^%s*$") == nil then 54 | return prev 55 | end 56 | prev = prev - 1 57 | end 58 | return -1 59 | end 60 | 61 | function M.try_find_parent(node, predicate, limit) 62 | limit = limit or math.huge 63 | local cur = node 64 | while limit >= 0 and cur do 65 | if predicate(cur) then 66 | return cur 67 | end 68 | cur = cur:parent() 69 | limit = limit - 1 70 | end 71 | end 72 | 73 | function M.is_line_empty(lnum, bufnr) 74 | local line = M.get_buf_line(bufnr, lnum) 75 | return #vim.trim(line) == 0 76 | end 77 | 78 | function M.get_first_nonblank_col_at_line(lnum, bufnr) 79 | local line = M.get_buf_line(bufnr, lnum) 80 | local _, col = string.find(line or "", "^%s*") 81 | return col or 0 82 | end 83 | 84 | function M.is_first_node_on_line(node, bufnr) 85 | local line, col = node:start() 86 | return M.get_first_nonblank_col_at_line(line, bufnr) >= col 87 | end 88 | 89 | -- Get the bootstrap language for the given line 90 | function M.get_lang_at_line(lnum, bufnr) 91 | local parser = M.get_parser(bufnr) 92 | local col = M.get_first_nonblank_col_at_line(lnum, bufnr) 93 | local lang_tree = parser:language_for_range({ lnum, col, lnum, col }) 94 | return lang_tree:lang() 95 | end 96 | 97 | function M.get_node_at_line(lnum, named, bufnr, filter) 98 | bufnr = bufnr or vim.api.nvim_get_current_buf() 99 | local col = M.get_first_nonblank_col_at_line(lnum, bufnr) 100 | 101 | local parser = M.get_parser(bufnr) 102 | 103 | local res_node 104 | local cur_root 105 | parser.for_each_tree(parser, function(tstree, lang_tree) 106 | local root = tstree:root() 107 | local rsr, rsc, rer, rec = root:range() 108 | if 109 | not M.range_contains(rsr, rsc, rer, rec, lnum, col, lnum, col + 1) 110 | or (cur_root and M.node_contains(root, cur_root)) 111 | then 112 | return 113 | end 114 | 115 | local node 116 | if named then 117 | node = root:named_descendant_for_range(lnum, col, lnum, col + 1) 118 | else 119 | node = root:descendant_for_range(lnum, col, lnum, col + 1) 120 | end 121 | 122 | -- make sure the returned node contains the range 123 | local sr, sc, er, ec = node:range() 124 | if not M.range_contains(sr, sc, er, ec, lnum, col, lnum, col + 1) then 125 | return 126 | end 127 | 128 | while node and filter and not filter(node, lang_tree:lang()) do 129 | node = node:parent() 130 | end 131 | if 132 | node --[[ (not res_node or M.node_contains(res_node, node)) ]] 133 | then 134 | res_node = node 135 | cur_root = root 136 | end 137 | end) 138 | 139 | return res_node 140 | end 141 | 142 | -- Do not use table here to drastically improve performance 143 | function M.range_contains(sr1, sc1, er1, ec1, sr2, sc2, er2, ec2) 144 | if sr1 > sr2 or (sr1 == sr2 and sc1 > sc2) then 145 | return false 146 | end 147 | if er1 < er2 or (er1 == er2 and ec1 < ec2) then 148 | return false 149 | end 150 | return true 151 | end 152 | 153 | function M.node_contains(node1, node2) 154 | local srow1, scol1, erow1, ecol1 = node1:range() 155 | local srow2, scol2, erow2, ecol2 = node2:range() 156 | return M.range_contains(srow1, scol1, erow1, ecol1, srow2, scol2, erow2, ecol2) 157 | end 158 | 159 | return M 160 | -------------------------------------------------------------------------------- /plugin/nvim-yati.vim: -------------------------------------------------------------------------------- 1 | lua << EOF 2 | require("nvim-yati").init() 3 | EOF 4 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | -------------------------------------------------------------------------------- /tests/fixtures/c/sample.c: -------------------------------------------------------------------------------- 1 | int x[] = { 2 | 1, 2, 3, 3 | 4, 5, 4 | 6 5 | }; 6 | 7 | int y[][2] = { 8 | {0, 1}, 9 | {1, 2}, 10 | { 11 | 2, 12 | 3 13 | MARKER 14 | }, 15 | }; 16 | 17 | /** 18 | * Fnction foo 19 | * @param[out] x output 20 | * @param[in] x input 21 | */ 22 | void foo(int *x, int y) { 23 | *x = y; 24 | if (x > 10) { 25 | if (x < 20) { 26 | MARKER 27 | } 28 | } else if (x < -10) { 29 | MARKER 30 | x = -10; 31 | } else { 32 | MARKER 33 | x = -x; 34 | 35 | if ( 36 | x && 37 | y && 38 | z && 39 | fs || 40 | (x && 41 | y) || 42 | (z && 43 | x) 44 | ) { 45 | return; 46 | } 47 | 48 | int z = (x + y) * 49 | (x - y); 50 | } 51 | } 52 | 53 | struct foo { 54 | int x, y; 55 | }; 56 | 57 | struct foo bar(int x, 58 | int y) { 59 | return (struct foo) { 60 | MARKER 61 | .x = x, 62 | .y = y 63 | }; 64 | } 65 | 66 | enum foo { 67 | A = 1, 68 | B, 69 | C, 70 | }; 71 | 72 | int 73 | foo(int a, int b) 74 | { 75 | goto error; 76 | return 0; 77 | error: 78 | MARKER 79 | while (x > 0) { 80 | x--; 81 | MARKER 82 | continue; 83 | } 84 | 85 | for ( 86 | int i = 0; 87 | MARKER 88 | i < 10; 89 | i++) 90 | cout << i; 91 | 92 | for (int i = 0; i < 5; ++i) { 93 | x++; 94 | MARKER 95 | break; 96 | } 97 | 98 | do { 99 | x++; 100 | } while (x < 0); 101 | 102 | } 103 | 104 | #define FOO(x) do { \ 105 | x = x + 1; \ 106 | x = x / 2; \ 107 | } while (x > 0); 108 | 109 | int foo(int x) { 110 | if (x > 10) 111 | return 10; 112 | else 113 | return x; 114 | 115 | while (1) 116 | x++; 117 | 118 | if (x) { 119 | if (y) { 120 | #if 1 121 | MARKER 122 | for (int i = 0; i < 3; ++i) 123 | x--; 124 | #else 125 | x++; 126 | #endif 127 | } 128 | } 129 | 130 | const char *a = "hello \ 131 | world"; 132 | 133 | const char *b = "hello " 134 | "world"; 135 | } 136 | 137 | struct foo { 138 | int a; 139 | struct bar { 140 | MARKER 141 | int x; 142 | } b; 143 | }; 144 | 145 | union baz { 146 | MARKER 147 | struct foo; 148 | int x; 149 | }; 150 | 151 | void foo(int x) { 152 | switch (x) { 153 | MARKER 154 | case 1: 155 | x += 1; 156 | break; 157 | case 2: 158 | x += 2; 159 | break; 160 | case 3: 161 | MARKER 162 | x += 3; 163 | break; 164 | case 4: { 165 | MARKER 166 | x += 4; 167 | break; 168 | } 169 | default: 170 | int y = (x > 10) 171 | ? 10 172 | : (x < -10) 173 | ? -10 174 | : x; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/fixtures/cpp/sample.cpp: -------------------------------------------------------------------------------- 1 | class Foo { 2 | public: 3 | class Bar { 4 | private: 5 | MARKER 6 | int x; 7 | void f() { 8 | cout 9 | MARKER 10 | << "at 1" 11 | << "at 1" 12 | << std::end; 13 | cin 14 | >> i; 15 | cin >> 16 | j; 17 | } 18 | }; 19 | private: 20 | int y; 21 | }; 22 | 23 | void foo() { 24 | auto f2 = [](int x, int y) 25 | -> int { 26 | return x + y; 27 | }; 28 | } 29 | 30 | namespace myspace { 31 | 32 | MARKER 33 | 34 | size_t hash< 35 | CharacterSet 36 | MARKER 37 | >::operator()(const CharacterSet &character_set) const { 38 | size_t result = 0; 39 | for (uint32_t c : 40 | MARKER 41 | character_set.included_chars) { 42 | hash_combine(&result, c); 43 | } 44 | } 45 | 46 | } 47 | 48 | extern "C" { 49 | 50 | MARKER 51 | void foo() { 52 | if (4 53 | + 5 < 10) { 54 | pass; 55 | } 56 | 57 | if (4 + 5 58 | < 10) { 59 | MARKER 60 | pass; 61 | } else { 62 | MARKER 63 | } 64 | 65 | for ( 66 | int i = 0; 67 | MARKER 68 | i < 10; 69 | i++) 70 | cout << i; 71 | 72 | doit 73 | MARKER 74 | .right 75 | .now(); 76 | 77 | try { 78 | dosome(); 79 | MARKRE 80 | } catch ( 81 | MARKER 82 | e 83 | ) { 84 | MARKER 85 | pass; 86 | } 87 | 88 | } 89 | } 90 | 91 | struct AltStruct 92 | { 93 | AltStruct(int x, double y): 94 | x_{x} 95 | , y_{y} 96 | {} 97 | }; 98 | 99 | template < 100 | MARKER 101 | typename Second 102 | > 103 | typedef SomeType TypedefName; 106 | 107 | typedef std::tuple test_tuple; 109 | 110 | -------------------------------------------------------------------------------- /tests/fixtures/css/sample.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | box-shadow: 0 0 0 1px #ccc, 3 | MARKER 4 | 0 0 0 2px #ccc; 5 | font-size: 12px; 6 | /* comment */ 7 | /* 8 | * comment 9 | * comment 10 | * */ 11 | } 12 | 13 | /* 14 | * comment 15 | * comment 16 | * */ 17 | 18 | .foo, 19 | .bar { 20 | MARKER 21 | color: #fff; 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/graphql/sample.graphql: -------------------------------------------------------------------------------- 1 | { 2 | args( 3 | var: $var 4 | MARKER 5 | ) 6 | fds( 7 | obj: { 8 | str: "hello" 9 | list: [ 10 | "hello" 11 | MARKER 12 | ] 13 | } 14 | ) 15 | } 16 | 17 | fragment Visit on HighlightedVisit 18 | @argumentDefinitions( 19 | count: { 20 | type: "Int", defaultValue: 20 21 | MARKER 22 | } 23 | ) { 24 | name 25 | } 26 | 27 | # Comment 28 | query claimsByBookingReferenceAndLastName( 29 | $lastName: String! 30 | # Comment 31 | ) { 32 | claimsByBookingReferenceAndLastName( 33 | bookingReference: $bookingReference 34 | ) { 35 | MARKER 36 | ...claim 37 | } 38 | } 39 | 40 | directive @a( 41 | as: String! = 1 @deprecated 42 | MARKER 43 | ) repeatable on QUERY | MUTATION 44 | 45 | enum State { 46 | PENDING 47 | VISIBLE 48 | MARKER 49 | } 50 | 51 | { 52 | posts { 53 | title 54 | votes 55 | author { 56 | firstName 57 | posts { 58 | author { 59 | firstName 60 | MARKER 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | type Type1 implements A, B, C, D { 68 | """ 69 | Description 70 | """ 71 | MARKER 72 | a: a 73 | } 74 | 75 | union longUnion = A 76 | | B 77 | MARKER 78 | | C 79 | 80 | union longUnion2 = 81 | | A 82 | MARKER 83 | | B 84 | | C 85 | -------------------------------------------------------------------------------- /tests/fixtures/html/sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 26 | 27 |
28 | MARKER 29 | 32 |

37 | MARKER 38 |

39 |
40 | 41 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/arrow_func_in_args.js: -------------------------------------------------------------------------------- 1 | someFunc((a1, a2) => 2 | anotherFunc( 3 | a1, 4 | MARKER 5 | a2, 6 | ), 7 | ) 8 | 9 | someFunc( 10 | 1111, 11 | (a1, a2) => 12 | anotherFunc( 13 | MARKER 14 | a1, 15 | a2, 16 | ), 17 | ) 18 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/basic.js: -------------------------------------------------------------------------------- 1 | import { 2 | a, 3 | b, 4 | MARKER 5 | c 6 | } from "mod"; 7 | 8 | export { 9 | a, 10 | b, 11 | MARKER 12 | } 13 | 14 | foo({ 15 | sd, 16 | aaa: sdf[ 17 | "index" + 18 | 2 19 | ] 20 | }, 21 | 4 22 | ); 23 | 24 | foo(2, { 25 | sd, 26 | sdf, 27 | MARKER 28 | }, 29 | 4 30 | ); 31 | 32 | foo( 2, 33 | { 34 | sd, 35 | sdf 36 | }); 37 | 38 | foo(2, { 39 | sd, 40 | sdf 41 | }); 42 | 43 | foo(2, { 44 | sd, 45 | sdf 46 | }, 'abc'); 47 | 48 | foo(2, 49 | { 50 | sd, 51 | sdf 52 | }, 53 | 'abc'); 54 | 55 | foo(2, 56 | 4); 57 | 58 | var x = [ 59 | 3, 60 | 4 61 | ]; 62 | 63 | const y = [ 64 | 1 65 | ]; 66 | 67 | const j = [{ 68 | a: 1 69 | }]; 70 | 71 | let h = { 72 | a: [ 1, 73 | MARKER 74 | 2 ], 75 | b: { j: [ 76 | { l: 1 }] 77 | }, 78 | c: 79 | { j: [ 80 | MARKER 81 | { l: 1 }] 82 | }, 83 | }; 84 | 85 | const a = 86 | { 87 | b: 1 88 | }; 89 | 90 | function func( 91 | fdsf 92 | ) { 93 | const f = () => { 94 | MARKER 95 | fs 96 | } 97 | } 98 | 99 | const af1 = (c) => { 100 | const a = () => { 101 | MARKER 102 | sdf 103 | } 104 | } 105 | 106 | const af2 = (c, 107 | b, 108 | d 109 | ) => ({ 110 | f: () => { 111 | MARKER 112 | sdf 113 | } 114 | }) 115 | 116 | class MyClass extends OtherComponent { 117 | 118 | state = { 119 | test: 1 120 | } 121 | 122 | constructor() { 123 | test(); 124 | } 125 | MARKER 126 | 127 | otherfunction = (a, b = { 128 | default: false 129 | }) => { 130 | more(); 131 | } 132 | } 133 | 134 | foo(myWrapper(mysecondWrapper({ 135 | /** 136 | * Comment 137 | * Comment 138 | */ 139 | a: 1 140 | }))); 141 | 142 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/binary.js: -------------------------------------------------------------------------------- 1 | b = 2 | 3 3 | + 5 4 | + 7 5 | + 8 6 | * 8 7 | * 9 8 | / 17 9 | * 8 10 | / 20 11 | - 34 12 | + 3 * 13 | 9 14 | - 8; 15 | 16 | ifthis 17 | && thendo() 18 | && aaaa 19 | || (otherwise 20 | && dothis 21 | && bbbb 22 | && cccc) && 23 | fff && 24 | fds || 25 | (fffff && 26 | foo( 27 | a, 28 | b, 29 | MARKER 30 | ) && 31 | aaa) 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/chained_call.js: -------------------------------------------------------------------------------- 1 | req 2 | .field 3 | MARKER 4 | .shouldBeOne() 5 | .abc(function() { 6 | MARKER 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/iife.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | return (function( 3 | a, 4 | b 5 | ) { 6 | MARKER 7 | ff 8 | })( 9 | c 10 | MARKER 11 | )( 12 | d 13 | MARKER 14 | )(123, function () { 15 | dosome() 16 | }, { 17 | a: 456 18 | }); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/injection.js: -------------------------------------------------------------------------------- 1 | function inject() { 2 | const style = css` 3 | .foo { 4 | MARKER 5 | color: red; 6 | } 7 | ` 8 | const query = gql` 9 | { 10 | args( 11 | var: $var 12 | MARKER 13 | ) 14 | fds( 15 | obj: { 16 | str: "hello" 17 | list: [ 18 | "hello" 19 | MARKER 20 | ] 21 | } 22 | ) 23 | } 24 | ` 25 | const interpolated = ` 26 | fdsfsaf 27 | ${ 28 | (function( 29 | a, 30 | b) { 31 | })( 32 | () => { 33 | MARKER 34 | return a + b 35 | } 36 | 2, 37 | ) 38 | } 39 | ` 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/jsx.js: -------------------------------------------------------------------------------- 1 | const jsx = ( 2 |
{ 8 | fd 9 | MARKER 10 | })() 11 | } 12 | MARKER 13 | > 14 |
15 | MARKER 16 | sdf 17 |
18 |
19 | {isA? ( 20 |
21 | aaaaaaaaaaaaaaa 22 |
) : ( 23 | fsdfsff 24 | MARKER 25 | ) 26 | } 27 |
28 |
29 | ); 30 | 31 | const a = ( 32 | 35 | ); 36 | 37 | const b = ( 38 | 40 | ); 41 | 42 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/jsx_ternary.fail.js: -------------------------------------------------------------------------------- 1 | const ttt = ( 2 |
3 | {isA? ( 4 |
5 | aaaaaaaaaaaaaaa 6 |
7 | ) : ( 8 | 9 | {/* 10 | * FIXME: extra indent 11 | */} 12 | fsdfsff 13 | MARKER 14 | 15 | )} 16 |
17 | ) 18 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/statements.js: -------------------------------------------------------------------------------- 1 | if (true) 2 | foo(); 3 | else 4 | bar(); 5 | 6 | if (true) { 7 | foo(); 8 | bar(); 9 | } else if ( 10 | false 11 | ) { 12 | (1 + 2) 13 | MARKER 14 | } else { 15 | MARKER 16 | foo(); 17 | } 18 | 19 | while (a === 1 20 | || b === 1) 21 | inLoop(); 22 | 23 | while (condition) 24 | inLoop(); 25 | after( 26 | aaaaaaaa 27 | ) 28 | .fooo // NOTE: this conforms to prettier behavior 29 | .bar(fffff); 30 | 31 | while (mycondition) { 32 | MARKER 33 | sdfsdfg(); 34 | } 35 | 36 | while (mycondition) { 37 | sdfsdfg(); 38 | if (test) { 39 | more() 40 | }} 41 | 42 | while (mycondition) 43 | if (test) { 44 | more() 45 | } 46 | 47 | switch (e) { 48 | case 4: 49 | MARKER 50 | case 5: 51 | something(); 52 | more(); 53 | case 6: 54 | somethingElse(); 55 | case 7: 56 | default: 57 | MARKER 58 | } 59 | 60 | for ( 61 | let i = 0; 62 | i < 10; 63 | i++ 64 | ) { 65 | for ( 66 | const { a, 67 | b, 68 | c, 69 | MARKER 70 | } of foo 71 | ) { 72 | MARKER 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/fixtures/javascript/ternary.js: -------------------------------------------------------------------------------- 1 | const af3 = (c) => 2 | ((d) => 3 | 123 4 | )( 5 | somebool 6 | ? foo( 7 | a, 8 | MARKER 9 | b 10 | ) 11 | : bar( 12 | c, 13 | d 14 | ) 15 | ? dd 16 | : baz( 17 | MARKER 18 | c 19 | ), 20 | anotherbool ? 21 | foo( 22 | a, 23 | MARKER 24 | b 25 | ) : bar( 26 | c, 27 | d 28 | ), 29 | MARKER 30 | b 31 | ) 32 | 33 | const conf = merge(base, { 34 | caaa: { 35 | aaaa: bbbb 36 | }, 37 | aaacccc: foo 38 | }, someCond() ? abc: { 39 | fooo, 40 | MARKER 41 | }) 42 | -------------------------------------------------------------------------------- /tests/fixtures/json/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "foo1": { 4 | "bar": "baz", 5 | "bar1": { 6 | MARKER 7 | "baz": "qux", 8 | "baz1": [ 9 | 123, 10 | 456 11 | MARKER 12 | ] 13 | } 14 | } 15 | MARKER 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/lua/sample.lua: -------------------------------------------------------------------------------- 1 | local tab = { 2 | as = { ff = "" }, 3 | ad = { 4 | a = "", 5 | b = { 6 | f = function() 7 | MARKER 8 | end, 9 | }, 10 | MARKER 11 | x = ({ 12 | f = (function() 13 | MARKER 14 | end), 15 | }) 16 | } 17 | } 18 | 19 | function foo(x) 20 | local bar = function(a, b, c) 21 | return (a 22 | + b 23 | + c 24 | ) 25 | end 26 | return bar( 27 | x, 28 | MARKER 29 | 1, 30 | 2) 31 | MARKER 32 | end 33 | 34 | local x = 35 | 10 36 | if x > 3 then 37 | MARKER 38 | x = 3 39 | elseif x < 3 then 40 | x = -3 41 | MARKER 42 | if 43 | x 44 | MARKER 45 | then 46 | accc() 47 | elseif 48 | abc > 9 49 | then 50 | if 51 | x 52 | and x * 5 53 | or x - 6 54 | MARKER 55 | then 56 | while df do 57 | for x, x in ipairs(aa) do 58 | cx() 59 | MARKER 60 | repeat 61 | x = x + 1 62 | until x > 100 63 | end 64 | MARKER 65 | end 66 | while 67 | aaa 68 | MARKER 69 | do 70 | fdsf() 71 | end 72 | end 73 | end 74 | else 75 | MARKER 76 | if x > 0 then 77 | local dd = 78 | 45 79 | end 80 | x = 0 81 | end 82 | 83 | -- comment line 84 | local function ffa( 85 | a, 86 | MARKER 87 | b 88 | ) 89 | function fff( 90 | a, 91 | b 92 | -- [[ 93 | -- block comment 94 | -- block comment 95 | -- ]] 96 | MARKER 97 | ) 98 | -- comment 99 | -- comment 100 | MARKER 101 | fdsf() 102 | end 103 | 104 | -- vim.cmd([[ 105 | -- startinsert 106 | -- ]]) 107 | 108 | local ss = [[ 109 | fdsafasf 110 | fdsagdgds 111 | ]] 112 | end 113 | 114 | function fun() 115 | Ins 116 | :method1( 117 | a, 118 | MARKER 119 | b 120 | ) 121 | :method2({ 122 | ffffffffffffff = 111, 123 | aaffs = function() 124 | foooo() 125 | end 126 | }, function() 127 | MARKER 128 | somecall() 129 | end) 130 | 131 | Ins2 132 | .method1( 133 | a, 134 | MARKER 135 | b 136 | ) 137 | .method2() 138 | end 139 | 140 | describe("foooooo", function() 141 | for _, bar in ipairs(bars) do 142 | MARKER 143 | describe("aaaaaaa", function() 144 | before_each(function() 145 | vim.cmd("aaaaaaaa") 146 | end) 147 | 148 | it("bbbbbbbbbbbb", function() 149 | MARKER 150 | end) 151 | end) 152 | end 153 | end) 154 | -------------------------------------------------------------------------------- /tests/fixtures/python/nested_align.py: -------------------------------------------------------------------------------- 1 | b = [ 2 | fooooooo, [[ 3 | 3 4 | MARKER 5 | ], 6 | ] 7 | ] 8 | 9 | b = ( 10 | 1, 3, [[3, 11 | MARKER 12 | ], 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /tests/fixtures/python/sample.py: -------------------------------------------------------------------------------- 1 | a = [ 2 | 1, 3 | [ 4 | MARKER 5 | 2, 6 | [ 7 | 3 8 | ] 9 | ] 10 | ] 11 | 12 | c = [[[ 13 | 3 14 | ]]] 15 | 16 | d = { 17 | 'a': [ 18 | 2, 3 19 | ], 20 | 'c': ( 21 | [1, 2, 3], 22 | [ 23 | 2, 24 | 4 25 | ], { 26 | 6, 27 | MARKER 28 | 8 29 | } 30 | ) 31 | } 32 | 33 | eeeeeeee = (1, 2, 34 | 3, 4, 35 | MARKER 36 | 5, 6) 37 | 38 | a = [ 39 | x + 1 for x in range(3) 40 | ] 41 | 42 | b = { 43 | x: x + 1 for x in range(3) 44 | } 45 | 46 | c = ( 47 | x * x for x in range(3) 48 | ) 49 | 50 | d = { 51 | x + x for x in range(3) 52 | } 53 | 54 | e = [ 55 | x + 1 for x 56 | in range(3) 57 | ] 58 | 59 | def fooaaaaaaaa(a, 60 | b, 61 | c): 62 | 63 | if a and b and c: 64 | MARKER 65 | pass 66 | elif a 67 | or b: 68 | MARKER 69 | also_works = True 70 | else: 71 | more(a, 72 | MARKER 73 | b, 74 | ) 75 | e = (1, 2, 76 | 3, 4, 77 | MARKER 78 | 5, 6 79 | ) 80 | MARKER 81 | 82 | baz = 'aaa' + \ 83 | 'fffff' + \ 84 | 'faaf' 85 | 86 | c = lambda x: \ 87 | x + 3 88 | 89 | match x: 90 | MARKER 91 | case {0: [1, 2, {}]}: 92 | y = 0 93 | case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: 94 | MARKER 95 | y = 1 96 | case []: 97 | y = 2 98 | 99 | return ( 100 | a, 101 | b 102 | ) 103 | 104 | while something(): 105 | x = 1 106 | g = 2 107 | MARKER 108 | 109 | while 0: 110 | x = 1 111 | g = 2 112 | else: 113 | x = 2 114 | MARKER 115 | g = 4 116 | 117 | # Comments 118 | # Comment 119 | 120 | try: 121 | # Comment 122 | 1/0 123 | MARKER 124 | except ZeroDivisionError: 125 | MARKER 126 | pass 127 | else: 128 | pass 129 | MARKER 130 | finally: 131 | pass 132 | MARKER 133 | 134 | def foo( 135 | a, 136 | c, 137 | d 138 | ): 139 | MARKER 140 | while True: 141 | if test: 142 | more() 143 | more() 144 | elif test2: 145 | more2() 146 | else: 147 | bar() 148 | if fsdf: 149 | fsfs 150 | fsdfa 151 | else: 152 | MARKER 153 | 154 | a = """ 155 | String A 156 | """ 157 | 158 | b = """ 159 | String B 160 | """ 161 | 162 | c = """ 163 | String C 164 | """ 165 | 166 | d = """ 167 | String D 168 | String D 169 | String D 170 | """ 171 | 172 | from os import ( 173 | path, 174 | MARKER 175 | name as OsName 176 | ) 177 | 178 | def foo(x, 179 | y, 180 | aligned_close 181 | MARKER 182 | ): 183 | pass 184 | 185 | class Foo: 186 | MARKER 187 | def __init__(self): 188 | pass 189 | 190 | def foo(self): 191 | if (bbbb or 192 | ffff is not None 193 | and aaaa > 0 194 | or fffff.b 195 | ): 196 | a = ( 197 | 1 198 | + 2 199 | - 3 200 | * 4 201 | * 5 202 | ) 203 | -------------------------------------------------------------------------------- /tests/fixtures/rust/micro.rs: -------------------------------------------------------------------------------- 1 | macro_rules! foo { 2 | ($a:ident, $b:ident, $c:ident) => { 3 | struct $a; 4 | struct $b; 5 | }, 6 | ($a:ident) => { 7 | struct $a; 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/rust/sample.rs: -------------------------------------------------------------------------------- 1 | const X: [i32; 2] = [ 2 | 1, 3 | MARKER 4 | 2, 5 | ]; 6 | 7 | fn foo( 8 | x: i32, 9 | MARKER 10 | y: i32, 11 | ) { 12 | if x > 10 { 13 | return 10; 14 | MARKER 15 | } else if x == 10 { 16 | MARKER 17 | return 9; 18 | } else { 19 | MARKER 20 | x += 10; 21 | } 22 | 23 | if let Some(x) = Some(10) { 24 | if x == -1 { 25 | MARKER 26 | return 0; 27 | } 28 | } else { 29 | MARKER 30 | return 1; 31 | } 32 | 33 | 0 34 | } 35 | 36 | enum Foo { 37 | X, 38 | Y( 39 | MARKER 40 | char, 41 | char, 42 | ), 43 | Z { 44 | x: u32, 45 | y: u32, 46 | MARKER 47 | }, 48 | } 49 | 50 | struct Foo { 51 | x: i32, 52 | y: i32, 53 | MARKER 54 | } 55 | 56 | impl Foo { 57 | fn foo() -> i32 { 58 | while x > 0 { 59 | x -= 1; 60 | for i in 0..3 { 61 | MARKER 62 | x += 1; 63 | loop { 64 | x += 1; 65 | if x < 100 { 66 | MARKER 67 | continue; 68 | } 69 | break; 70 | } 71 | } 72 | } 73 | 74 | let mut trie = vec![TrieNode { 75 | is_end: false, 76 | MARKER 77 | next: [None; 26], 78 | }]; 79 | 80 | } 81 | MARKER 82 | } 83 | 84 | trait Bar { 85 | fn bar(); 86 | MARKER 87 | } 88 | 89 | foo! { 90 | (bar) => { 91 | MARKER 92 | } 93 | } 94 | 95 | fn foo(x: i32) -> i32 { 96 | match x { 97 | 0 => 1, 98 | 1 => { 99 | MARKER 100 | 2 101 | }, 102 | 2 | 3 => { 103 | 4 104 | } 105 | } 106 | } 107 | 108 | mod foo { 109 | const X: i32 = 1; 110 | mod bar { 111 | MARKER 112 | const Y: i32 = 1; 113 | } 114 | } 115 | 116 | fn foo() { 117 | let a = "hello 118 | world"; 119 | 120 | let b = "hello\ 121 | world"; 122 | 123 | let c = r#" 124 | hello 125 | world 126 | "#; 127 | } 128 | 129 | fn foo(t: T) -> i32 130 | where 131 | T: Debug, 132 | MARKER 133 | U: Integer, 134 | { 135 | 1 136 | } 137 | 138 | fn foo(t: T) -> i32 where 139 | T: Debug, 140 | { 141 | let paths: Vec<_> = ({ 142 | fs::read_dir("test_data") 143 | .unwrap() 144 | .clone(|aaaaa| { 145 | MARKER 146 | }) 147 | MARKER 148 | .cloned() 149 | }) 150 | .collect(); 151 | 152 | statement(); 153 | } 154 | 155 | // Comment 156 | /* 157 | * comment 158 | * comment 159 | */ 160 | impl Write for Foo 161 | where 162 | T: Debug, 163 | { 164 | // Comment 165 | } 166 | 167 | fn foo() { 168 | {{{ 169 | let explicit_arg_decls = 170 | explicit_arguments.into_iter() 171 | .enumerate() 172 | .map(|(index, (ty, pattern))| { 173 | let lvalue = Lvalue::Arg(index as u32); 174 | block = this.pattern(block, 175 | argument_extent, 176 | MARKER 177 | hair::PatternRef::Hair(pattern), 178 | &lvalue); 179 | ArgDecl { ty: ty } 180 | }); 181 | }}} 182 | } 183 | 184 | fn f< 185 | X, 186 | MARKER 187 | Y 188 | >() { 189 | g(|_| { 190 | let x: HashMap< 191 | String, 192 | MARKER 193 | String, 194 | >::new(); 195 | h(); 196 | }) 197 | .unwrap(); 198 | h(); 199 | } 200 | 201 | fn floaters() { 202 | let x = Foo { 203 | field1: val1, 204 | field2: val2, 205 | } 206 | .method_call().method_call(); 207 | 208 | let y = if cond { 209 | val1 210 | } else { 211 | val2 212 | } 213 | .await 214 | .method_call([ 215 | 1, 216 | 3, 217 | MARKER 218 | 1 219 | ]); 220 | 221 | x = 222 | 456 223 | + 789 224 | + 111 225 | - 222; 226 | 227 | // NOTE: rustfmt do not expand binary expression 228 | if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 229 | && bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 230 | || ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc 231 | && ecccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc 232 | { 233 | } 234 | 235 | { 236 | match x { 237 | PushParam => { 238 | // comment 239 | stack.push(mparams[match cur.to_digit(10) { 240 | Some(d) => d as usize - 1, 241 | None => return Err("bad param number".to_owned()), 242 | }] 243 | .clone(|aaaaa| { 244 | MARKER 245 | }) 246 | MARKER 247 | .await 248 | MARKER 249 | .unwrap() 250 | ); 251 | } 252 | } 253 | 254 | if let Some(frame) = match connection.read_frame().await { 255 | Ok(it) => it, 256 | Err(err) => return Err(err), 257 | MARKER 258 | } { 259 | println!("Got {}", frame); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /tests/fixtures/toml/sample.toml: -------------------------------------------------------------------------------- 1 | multiLine = [ 2 | 1, 3 | MARKER 4 | 2, 5 | { a = "str", b = "str" }, 6 | ] 7 | -------------------------------------------------------------------------------- /tests/fixtures/tsx/sample.tsx: -------------------------------------------------------------------------------- 1 | export function List() { 2 | return ( 3 | 4 |
5 | { 6 | MARKER 7 | } 8 |
9 | { 10 | MARKER 11 | } 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/typescript/basic.ts: -------------------------------------------------------------------------------- 1 | interface Foo { 2 | bar: string; 3 | MARKER 4 | fun: ( 5 | a: { 6 | MARKER 7 | b: string; 8 | c: number; 9 | }, 10 | b: [ 11 | number, 12 | MARKER 13 | { 14 | a: Array; 15 | } 16 | ], 17 | c 18 | ) => { 19 | MARKER 20 | baz: string; 21 | }; 22 | } 23 | 24 | interface Bar 25 | { 26 | aaa: a extends object 27 | ? a 28 | : c extends AAA 29 | ? foo 30 | : bar; 31 | } 32 | 33 | type Foo2 = { 44 | bar: string; 45 | baz: Record< 46 | A, 47 | B 48 | > 49 | } 50 | 51 | enum e { 52 | a, 53 | MARKER 54 | b, 55 | } 56 | 57 | type RequestType = 58 | | "GET" 59 | | "HEAD" 60 | | "POST" 61 | MARKER 62 | | "PUT" 63 | | "OPTIONS" 64 | | "CONNECT" 65 | | "DELETE" 66 | | "TRACE"; 67 | 68 | type Union2 = "GET" 69 | MARKER 70 | | "HEAD" 71 | | "POST"; 72 | -------------------------------------------------------------------------------- /tests/fixtures/typescript/return_type.ts: -------------------------------------------------------------------------------- 1 | export function func( 2 | aaa: number, 3 | bbb: boolean 4 | ): { 5 | ac: T; 6 | pppp: P; 7 | MARKER 8 | } { 9 | MARKER 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/vue/sample.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 26 | 27 | 41 | 42 | 50 | 51 | -------------------------------------------------------------------------------- /tests/helper.lua: -------------------------------------------------------------------------------- 1 | local assert = require("luassert") 2 | local say = require("say") 3 | local get_indent = require("nvim-yati.indent").get_indent 4 | local scandir = require("plenary.scandir").scan_dir 5 | local utils = require("nvim-yati.utils") 6 | local logger = require("nvim-yati.logger") 7 | 8 | local test_file_dir = "tests/fixtures/" 9 | local ignore_pattern = ".*%.fail%..*" 10 | local test_langs = { 11 | "c", 12 | "cpp", 13 | "graphql", 14 | "html", 15 | "javascript", 16 | "json", 17 | "lua", 18 | "python", 19 | "rust", 20 | "toml", 21 | "tsx", 22 | "typescript", 23 | "vue", 24 | } 25 | 26 | local M = {} 27 | 28 | local function same_indent(state, arguments) 29 | local lnum = arguments[1] 30 | local expected = arguments[2] 31 | 32 | logger("TEST_START", "Line " .. lnum) 33 | local indent = get_indent(lnum - 1) 34 | logger("TEST_END", "Indent " .. indent) 35 | return indent == expected 36 | end 37 | 38 | ---@param bufnr number buffer number 39 | ---@param marker_str string marker for the empty line 40 | ---@return table empty_indents a lnum to indent size map 41 | local function extract_marker(bufnr, marker_str) 42 | local empty_indents = {} 43 | local content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 44 | 45 | for lnum, line in ipairs(content) do 46 | -- count the number of spaces at the beginning of the line and trim the line 47 | local _, indent = line:find("^%s*") 48 | if line:sub(indent + 1) == marker_str then 49 | empty_indents[lnum] = indent 50 | vim.api.nvim_buf_set_lines(bufnr, lnum - 1, lnum, true, { "" }) 51 | end 52 | end 53 | 54 | return empty_indents 55 | end 56 | 57 | function M.expected_indents_iter(marker_str, bufnr) 58 | local line_cnt = vim.api.nvim_buf_line_count(bufnr) 59 | local empty_indents = extract_marker(bufnr, marker_str) 60 | 61 | -- full parse before testing (for injection) 62 | local parser = vim.treesitter.get_parser(bufnr) 63 | parser:parse(true) 64 | 65 | local lnum = 0 66 | return function() 67 | while lnum < line_cnt do 68 | lnum = lnum + 1 69 | local expected = utils.cur_indent(lnum - 1, bufnr) 70 | if empty_indents[lnum] then 71 | return lnum, empty_indents[lnum] 72 | elseif expected ~= 0 then 73 | return lnum, expected 74 | end 75 | end 76 | end 77 | end 78 | 79 | function M.get_test_langs() 80 | return test_langs 81 | end 82 | 83 | function M.get_test_files(lang) 84 | local files = scandir(test_file_dir .. lang) 85 | return vim.tbl_filter(function(file) 86 | return vim.fs.basename(file):find(ignore_pattern) == nil 87 | end, files) 88 | end 89 | 90 | function M.basename(path) 91 | return vim.fs.basename(path) 92 | end 93 | 94 | function M.setup() 95 | say:set_namespace("en") 96 | say:set("assertion.same_indent.negative", "Line %s didn't indent to %s.") 97 | say:set("assertion.same_indent.positive", "Line %s didn't indent to %s.") 98 | 99 | assert:register( 100 | "assertion", 101 | "same_indent", 102 | same_indent, 103 | "assertion.same_indent.positive", 104 | "assertion.same_indent.negative" 105 | ) 106 | end 107 | 108 | return M 109 | -------------------------------------------------------------------------------- /tests/indent_spec.lua: -------------------------------------------------------------------------------- 1 | local helper = require("tests.helper") 2 | 3 | helper.setup() 4 | 5 | for _, lang in ipairs(helper.get_test_langs()) do 6 | describe(lang, function() 7 | after_each(function() 8 | vim.cmd("bdelete!") 9 | end) 10 | 11 | for _, file in ipairs(helper.get_test_files(lang)) do 12 | it(string.format("indent should be correct [%s]", helper.basename(file)), function() 13 | vim.cmd("edit! " .. file) 14 | for lnum, indent in helper.expected_indents_iter("MARKER", 0) do 15 | assert.same_indent(lnum, indent) 16 | end 17 | end) 18 | end 19 | end) 20 | end 21 | -------------------------------------------------------------------------------- /tests/install.vim: -------------------------------------------------------------------------------- 1 | " https://github.com/neovim/neovim/issues/12432 2 | set display=lastline 3 | 4 | set packpath+=./deps 5 | set rtp+=. 6 | 7 | packloadall 8 | runtime plugin/nvim-yati.vim 9 | 10 | lua << EOF 11 | local parsers = require('nvim-treesitter.parsers') 12 | local config = require('nvim-yati.config') 13 | for _, lang in ipairs(parsers.available_parsers()) do 14 | if 15 | config.is_supported(lang) 16 | and #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", false) == 0 17 | then 18 | vim.cmd("TSInstallSync " .. lang) 19 | end 20 | end 21 | EOF 22 | -------------------------------------------------------------------------------- /tests/lazy_indent_spec.lua: -------------------------------------------------------------------------------- 1 | local helper = require("tests.helper") 2 | 3 | helper.setup() 4 | 5 | require("nvim-treesitter.configs").setup({ 6 | yati = { 7 | default_lazy = true, 8 | }, 9 | }) 10 | 11 | for _, lang in ipairs(helper.get_test_langs()) do 12 | describe(lang, function() 13 | after_each(function() 14 | vim.cmd("bdelete!") 15 | end) 16 | 17 | for _, file in ipairs(helper.get_test_files(lang)) do 18 | it(string.format("indent should be correct [%s]", helper.basename(file)), function() 19 | vim.cmd("edit! " .. file) 20 | for lnum, indent in helper.expected_indents_iter("MARKER", 0) do 21 | assert.same_indent(lnum, indent) 22 | end 23 | end) 24 | end 25 | end) 26 | end 27 | -------------------------------------------------------------------------------- /tests/preload.vim: -------------------------------------------------------------------------------- 1 | set noswapfile 2 | set directory="" 3 | set display=lastline 4 | 5 | set packpath+=./deps 6 | set rtp+=. 7 | 8 | set shiftwidth=2 9 | set expandtab 10 | 11 | packloadall 12 | runtime plugin/nvim-yati.vim 13 | 14 | lua << EOF 15 | -- require("nvim-yati.debug").toggle() 16 | EOF 17 | --------------------------------------------------------------------------------