├── .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 |
28 | MARKER
29 |
30 |
35 | {{
36 | MARKER
37 | some interpolation
38 | MARKER
39 | }}
40 |
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 |
--------------------------------------------------------------------------------