diff --git a/.cbfmt.toml b/.cbfmt.toml new file mode 100644 index 00000000..bd325e4a --- /dev/null +++ b/.cbfmt.toml @@ -0,0 +1,2 @@ +[languages] +lua = ["stylua -s -"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..c58fbd08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,40 @@ +name: Bug Report +description: Report a problem with indent-blankline +labels: [bug] +body: + - type: markdown + attributes: + value: | + _Before reporting:_ Make sure you are on the latest version of the plugin, and either the latest stable or nightly release of Neovim. Search [existing issues](https://github.com/lukas-reineke/indent-blankline.nvim/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) + + - type: textarea + attributes: + label: 'Problem' + description: 'Describe the current behavior. Include images, or videos if possible.' + validations: + required: true + - type: textarea + attributes: + label: 'Steps to reproduce' + description: | + - Share your configurtaion and describe the steps to reproduce the issue. + - See [Minimal-reproduction-template](https://github.com/lukas-reineke/indent-blankline.nvim/wiki/Minimal-reproduction-template#minimal-config) for how to create a minimal configuration. + placeholder: | + nvim --clean + :edit foo + yiwp + validations: + required: true + - type: textarea + attributes: + label: 'Expected behavior' + description: 'Describe the behavior you expect.' + validations: + required: true + + - type: input + attributes: + label: 'Neovim version (nvim -v)' + placeholder: '0.6.0 commit db1b0ee3b30f' + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..cd706443 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: Feature request +description: Request an enhancement for indent-blankline +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Before requesting: search [existing issues](https://github.com/lukas-reineke/indent-blankline.nvim/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and read the documentation `:help indent-blankline`. + + - type: textarea + attributes: + label: 'Problem' + description: 'Describe the problem to be solved.' + placeholder: 'Add bongocat' + validations: + required: true + + - type: textarea + attributes: + label: 'Expected behavior' + description: 'Describe what the new feature or behavior would look like. How does it solve the problem? Is it worth the cost?' + validations: + required: true diff --git a/.github/workflows/nightly-check.yml b/.github/workflows/nightly-check.yml new file mode 100644 index 00000000..14826ddb --- /dev/null +++ b/.github/workflows/nightly-check.yml @@ -0,0 +1,60 @@ +name: Nightly Neovim check +# Checks LSP and unit tests against new Neovim nightly once a week + +on: + schedule: + - cron: '30 21 * * 0' # 6:30 AM JST, Monday + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: unit tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/v0.9.1/nvim-linux64.tar.gz" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } + + - name: Run tests + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + nvim --version + make test + + lua-language-server: + name: lua language server + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Restore cache for lua LS + uses: actions/cache@v3 + with: + path: _lua-ls + key: 3.7.0 + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } + test -d _lua-ls || { + mkdir -p _lua-ls + curl -sL "https://github.com/LuaLS/lua-language-server/releases/download/3.7.0/lua-language-server-3.7.0-linux-x64.tar.gz" | tar xzf - -C "${PWD}/_lua-ls" + } + + - name: Run check + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export PATH="${PWD}/_lua-ls/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + nvim --version + make lua-language-server version=nightly diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 8048e583..4e4fb235 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -3,41 +3,122 @@ name: Pull request check on: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - format: - runs-on: ubuntu-latest + tests: + name: unit tests + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - version: nightly + rev: nightly/nvim-linux64.tar.gz + - version: stable + rev: v0.9.2/nvim-linux64.tar.gz steps: - - uses: actions/checkout@v2 - - uses: JohnnyMorganz/stylua-action@1.0.0 + - uses: actions/checkout@v3 + - run: date +%F > todays-date + - name: Restore cache for today's nightly. + uses: actions/cache@v3 with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --check . + path: _neovim + key: ${{ matrix.rev }}-${{ hashFiles('todays-date') }} + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } - block-fixup: - runs-on: ubuntu-latest + - name: Run tests + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + nvim --version + make test + lua-language-server: + name: lua language server + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - version: nightly + rev: nightly/nvim-linux64.tar.gz + - version: stable + rev: v0.9.2/nvim-linux64.tar.gz steps: - - uses: actions/checkout@v2 - - name: Block Fixup Commit Merge - uses: 13rac1/block-fixup-merge-action@v2.0.0 + - uses: actions/checkout@v3 + - run: date +%F > todays-date + - name: Restore cache for today's nightly. + uses: actions/cache@v3 + with: + path: _neovim + key: ${{ matrix.rev }}-${{ hashFiles('todays-date') }} + - name: Restore vendor dependencies + uses: actions/cache@v3 + with: + path: vendor + key: ${{ hashFiles('todays-date') }} + - name: Restore cache for lua LS + uses: actions/cache@v3 + with: + path: _lua-ls + key: 3.7.0 + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } + test -d _lua-ls || { + mkdir -p _lua-ls + curl -sL "https://github.com/LuaLS/lua-language-server/releases/download/3.7.0/lua-language-server-3.7.0-linux-x64.tar.gz" | tar xzf - -C "${PWD}/_lua-ls" + } - luacheck: - runs-on: ubuntu-latest + - name: Run check + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export PATH="${PWD}/_lua-ls/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + nvim --version + make lua-language-server version=${{ matrix.version }} + stylua: + name: stylua + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@master - - - uses: leafo/gh-actions-lua@v8.0.0 + - uses: actions/checkout@v3 + - uses: JohnnyMorganz/stylua-action@v3 with: - luaVersion: 'luajit-2.1.0-beta3' + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + args: --color always --check . - - uses: leafo/gh-actions-luarocks@v4.0.0 + luacheck: + name: luacheck + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 - - name: build + - name: Prepare run: | - luarocks install luacheck + sudo apt-get update + sudo apt-get install -y luarocks + sudo luarocks install luacheck - - name: test - run: | - luacheck lua + - name: Lint + run: sudo make luacheck + block-fixup: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + - name: Block Fixup Commit Merge + uses: 13rac1/block-fixup-merge-action@v2.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ffc59e07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor/* +lua-language-server-log/* +_neovim/* +_lua-ls/* diff --git a/.luacheckrc b/.luacheckrc index 35b522b5..8d1654fd 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,2 +1,6 @@ -globals = { "vim", "_" } +globals = { "vim", "_", "describe", "it", "after_each", "before_each", "assert" } max_line_length = false + +exclude_files = { + "vendor", +} diff --git a/.luarc.nightly.json b/.luarc.nightly.json new file mode 100644 index 00000000..c64181a3 --- /dev/null +++ b/.luarc.nightly.json @@ -0,0 +1,18 @@ +{ + "runtime.version": "LuaJIT", + "diagnostics.globals": [ + "it", + "describe", + "before_each", + "after_each", + "setup", + "teardown" + ], + "diagnostics.ignoredFiles": "Disable", + "diagnostics.libraryFiles": "Disable", + "workspace.library": [ + "/usr/local/share/nvim/runtime/lua", + "_neovim/share/nvim/runtime/lua", + "vendor/pack/vendor/start/neodev.nvim/types/nightly" + ] +} diff --git a/.luarc.stable.json b/.luarc.stable.json new file mode 100644 index 00000000..3e89d74d --- /dev/null +++ b/.luarc.stable.json @@ -0,0 +1,18 @@ +{ + "runtime.version": "LuaJIT", + "diagnostics.globals": [ + "it", + "describe", + "before_each", + "after_each", + "setup", + "teardown" + ], + "diagnostics.ignoredFiles": "Disable", + "diagnostics.libraryFiles": "Disable", + "workspace.library": [ + "/usr/local/share/nvim/runtime/lua", + "_neovim/share/nvim/runtime/lua", + "vendor/pack/vendor/start/neodev.nvim/types/stable" + ] +} diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 00000000..ffc59e07 --- /dev/null +++ b/.styluaignore @@ -0,0 +1,4 @@ +vendor/* +lua-language-server-log/* +_neovim/* +_lua-ls/* diff --git a/LICENSE.md b/LICENSE.md index 7074561a..f49efce5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT Licence -Copyright (c) 2022 Lukas Reineke +Copyright (c) 2023 Lukas Reineke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..95b61440 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +ifndef VERBOSE +.SILENT: +endif + +test: dependencies + @echo "Running indent-blankline tests..." + timeout 300 nvim -e \ + --headless \ + --noplugin \ + -u specs/spec.lua \ + -c "PlenaryBustedDirectory specs/features {minimal_init = 'specs/spec.lua'}" + +luacheck: + luacheck . + +stylua: + stylua --check . + +lua-language-server: dependencies + rm -rf lua-language-server-log + lua-language-server --configpath .luarc.$(version).json --logpath lua-language-server-log --check . + [ -f lua-language-server-log/check.json ] && { cat lua-language-server-log/check.json 2>/dev/null; exit 1; } || true + +dependencies: + if [ ! -d vendor ]; then \ + git clone --depth 1 \ + https://github.com/nvim-lua/plenary.nvim \ + vendor/pack/vendor/start/plenary.nvim; \ + git clone --depth 1 \ + https://github.com/folke/neodev.nvim \ + vendor/pack/vendor/start/neodev.nvim; \ + fi diff --git a/README.md b/README.md index a139e0d0..df2d1505 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ # Indent Blankline -This plugin adds indentation guides to all lines (including empty lines). - +This plugin adds indentation guides to Neovim. It uses Neovim's virtual text feature and **no conceal** -This plugin requires Neovim 0.5 or higher. It makes use of Neovim only -features so it will not work in Vim. -There is a legacy version of the plugin that supports Neovim 0.4 under the -branch `version-1` +To start using indent-blankline, call the `ibl.setup()` function. + +This plugin requires the latest stable version of Neovim. ## Install @@ -16,143 +14,139 @@ Use your favourite plugin manager to install. For [lazy.nvim](https://github.com/folke/lazy.nvim): ```lua -{ "lukas-reineke/indent-blankline.nvim" }, +{ "lukas-reineke/indent-blankline.nvim", opts = {} }, ``` -For [packer.nvim](https://github.com/wbthomason/packer.nvim): +For [pckr.nvim](https://github.com/lewis6991/pckr.nvim): ```lua use "lukas-reineke/indent-blankline.nvim" ``` -For [vim-plug](https://github.com/junegunn/vim-plug): - -```vim -Plug 'lukas-reineke/indent-blankline.nvim' -``` - ## Setup -To configure indent-blankline, either run the setup function, or set variables manually. -The setup function has a single table as argument, keys of the table match the `:help indent-blankline-variables` without the `indent_blankline_` part. +To initialize and configure indent-blankline, run the `setup` function. ```lua -require("indent_blankline").setup { - -- for example, context is off by default, use this to turn it on - show_current_context = true, - show_current_context_start = true, -} +require("ibl").setup() ``` -Please see `:help indent_blankline.txt` for more details and all possible values. - -A lot of [Yggdroot/indentLine](https://github.com/Yggdroot/indentLine) options should work out of the box. +Optionally, you can pass a configuration table to the setup function. For all +available options, take a look at `:help ibl.config`. ## Screenshots -All screenshots use [my custom onedark color scheme](https://github.com/lukas-reineke/onedark.nvim). - ### Simple ```lua -vim.opt.list = true -vim.opt.listchars:append "eol:↴" - -require("indent_blankline").setup { - show_end_of_line = true, -} +require("ibl").setup() ``` -Screenshot +Screenshot -#### With custom `listchars` and `g:indent_blankline_space_char_blankline` +### Scope -```lua -vim.opt.list = true -vim.opt.listchars:append "space:⋅" -vim.opt.listchars:append "eol:↴" +Scope requires treesitter to be setup. -require("indent_blankline").setup { - show_end_of_line = true, - space_char_blankline = " ", -} +```lua +require("ibl").setup() ``` -Screenshot +Screenshot -#### With custom `g:indent_blankline_char_highlight_list` +The start and end of scope uses underline, so to achieve the best result you +might need to tweak the underline position. In Kitty terminal for example you +can do that with [modify_font](https://sw.kovidgoyal.net/kitty/conf/#opt-kitty.modify_font) + +### Mixed indentation ```lua -vim.opt.termguicolors = true -vim.cmd [[highlight IndentBlanklineIndent1 guifg=#E06C75 gui=nocombine]] -vim.cmd [[highlight IndentBlanklineIndent2 guifg=#E5C07B gui=nocombine]] -vim.cmd [[highlight IndentBlanklineIndent3 guifg=#98C379 gui=nocombine]] -vim.cmd [[highlight IndentBlanklineIndent4 guifg=#56B6C2 gui=nocombine]] -vim.cmd [[highlight IndentBlanklineIndent5 guifg=#61AFEF gui=nocombine]] -vim.cmd [[highlight IndentBlanklineIndent6 guifg=#C678DD gui=nocombine]] - -vim.opt.list = true -vim.opt.listchars:append "space:⋅" -vim.opt.listchars:append "eol:↴" - -require("indent_blankline").setup { - space_char_blankline = " ", - char_highlight_list = { - "IndentBlanklineIndent1", - "IndentBlanklineIndent2", - "IndentBlanklineIndent3", - "IndentBlanklineIndent4", - "IndentBlanklineIndent5", - "IndentBlanklineIndent6", - }, -} +require("ibl").setup() ``` -Screenshot +Screenshot -#### With custom background highlight +### Multiple indent colors ```lua -vim.opt.termguicolors = true -vim.cmd [[highlight IndentBlanklineIndent1 guibg=#1f1f1f gui=nocombine]] -vim.cmd [[highlight IndentBlanklineIndent2 guibg=#1a1a1a gui=nocombine]] - -require("indent_blankline").setup { - char = "", - char_highlight_list = { - "IndentBlanklineIndent1", - "IndentBlanklineIndent2", - }, - space_char_highlight_list = { - "IndentBlanklineIndent1", - "IndentBlanklineIndent2", - }, - show_trailing_blankline_indent = false, +local highlight = { + "RainbowRed", + "RainbowYellow", + "RainbowBlue", + "RainbowOrange", + "RainbowGreen", + "RainbowViolet", + "RainbowCyan", } + +local hooks = require "ibl.hooks" +-- create the highlight groups in the highlight setup hook, so they are reset +-- every time the colorscheme changes +hooks.register(hooks.type.HIGHLIGHT_SETUP, function() + vim.api.nvim_set_hl(0, "RainbowRed", { fg = "#E06C75" }) + vim.api.nvim_set_hl(0, "RainbowYellow", { fg = "#E5C07B" }) + vim.api.nvim_set_hl(0, "RainbowBlue", { fg = "#61AFEF" }) + vim.api.nvim_set_hl(0, "RainbowOrange", { fg = "#D19A66" }) + vim.api.nvim_set_hl(0, "RainbowGreen", { fg = "#98C379" }) + vim.api.nvim_set_hl(0, "RainbowViolet", { fg = "#C678DD" }) + vim.api.nvim_set_hl(0, "RainbowCyan", { fg = "#56B6C2" }) +end) + +require("ibl").setup { indent = { highlight = highlight } } ``` -Screenshot +Screenshot -#### With context indent highlighted by treesitter +### Background color indentation guides ```lua -vim.opt.list = true -vim.opt.listchars:append "space:⋅" -vim.opt.listchars:append "eol:↴" - -require("indent_blankline").setup { - space_char_blankline = " ", - show_current_context = true, - show_current_context_start = true, +local highlight = { + "CursorColumn", + "Whitespace", +} +require("ibl").setup { + indent = { highlight = highlight, char = "" }, + whitespace = { + highlight = highlight, + remove_blankline_trail = false, + }, + scope = { enabled = false }, } ``` -Screenshot +Screenshot -`show_current_context_start` uses underline, so to achieve the best result you -might need to tweak the underline position. In Kitty terminal for example you -can do that with [modify_font](https://sw.kovidgoyal.net/kitty/conf/#opt-kitty.modify_font) +### rainbow-delimiters.nvim integration + +[rainbow-delimiters.nvim](https://gitlab.com/HiPhish/rainbow-delimiters.nvim) -## Thanks +```lua +local highlight = { + "RainbowRed", + "RainbowYellow", + "RainbowBlue", + "RainbowOrange", + "RainbowGreen", + "RainbowViolet", + "RainbowCyan", +} +local hooks = require "ibl.hooks" +-- create the highlight groups in the highlight setup hook, so they are reset +-- every time the colorscheme changes +hooks.register(hooks.type.HIGHLIGHT_SETUP, function() + vim.api.nvim_set_hl(0, "RainbowRed", { fg = "#E06C75" }) + vim.api.nvim_set_hl(0, "RainbowYellow", { fg = "#E5C07B" }) + vim.api.nvim_set_hl(0, "RainbowBlue", { fg = "#61AFEF" }) + vim.api.nvim_set_hl(0, "RainbowOrange", { fg = "#D19A66" }) + vim.api.nvim_set_hl(0, "RainbowGreen", { fg = "#98C379" }) + vim.api.nvim_set_hl(0, "RainbowViolet", { fg = "#C678DD" }) + vim.api.nvim_set_hl(0, "RainbowCyan", { fg = "#56B6C2" }) +end) + +vim.g.rainbow_delimiters = { highlight = highlight } +require("ibl").setup { scope = { highlight = highlight } } + +hooks.register(hooks.type.SCOPE_HIGHLIGHT, hooks.builtin.scope_highlight_from_extmark) +``` -Special thanks to [Yggdroot/indentLine](https://github.com/Yggdroot/indentLine) +Screenshot diff --git a/after/ftplugin/c.lua b/after/ftplugin/c.lua new file mode 100644 index 00000000..9cff9bbf --- /dev/null +++ b/after/ftplugin/c.lua @@ -0,0 +1,3 @@ +local hooks = require "ibl.hooks" + +hooks.register(hooks.type.SKIP_LINE, hooks.builtin.skip_preproc_lines, { bufnr = 0 }) diff --git a/after/ftplugin/cpp.lua b/after/ftplugin/cpp.lua new file mode 100644 index 00000000..9cff9bbf --- /dev/null +++ b/after/ftplugin/cpp.lua @@ -0,0 +1,3 @@ +local hooks = require "ibl.hooks" + +hooks.register(hooks.type.SKIP_LINE, hooks.builtin.skip_preproc_lines, { bufnr = 0 }) diff --git a/after/plugin/commands.lua b/after/plugin/commands.lua new file mode 100644 index 00000000..19b7c773 --- /dev/null +++ b/after/plugin/commands.lua @@ -0,0 +1,52 @@ +local ibl = require "ibl" +local conf = require "ibl.config" + +vim.api.nvim_create_user_command("IBLEnable", function() + ibl.update { enabled = true } +end, { + bar = true, + desc = "Enables indent-blankline", +}) + +vim.api.nvim_create_user_command("IBLDisable", function() + ibl.update { enabled = false } +end, { + bar = true, + desc = "Disables indent-blankline", +}) + +vim.api.nvim_create_user_command("IBLToggle", function() + if ibl.initialized then + ibl.update { enabled = not conf.get_config(-1).enabled } + else + ibl.setup {} + end +end, { + bar = true, + desc = "Toggles indent-blankline on and off", +}) + +vim.api.nvim_create_user_command("IBLEnableScope", function() + ibl.update { scope = { enabled = true } } +end, { + bar = true, + desc = "Enables indent-blanklines scope", +}) + +vim.api.nvim_create_user_command("IBLDisableScope", function() + ibl.update { scope = { enabled = false } } +end, { + bar = true, + desc = "Disables indent-blanklines scope", +}) + +vim.api.nvim_create_user_command("IBLToggleScope", function() + if ibl.initialized then + ibl.update { scope = { enabled = not conf.get_config(-1).scope.enabled } } + else + ibl.setup {} + end +end, { + bar = true, + desc = "Toggles indent-blanklines scope on and off", +}) diff --git a/doc/indent_blankline.txt b/doc/indent_blankline.txt index bf7ba3a2..96750e8b 100644 --- a/doc/indent_blankline.txt +++ b/doc/indent_blankline.txt @@ -1,789 +1,816 @@ -*indent_blankline.txt* Show vertical lines for indent on empty lines +*indent-blankline.txt* Adds indentation guides to Neovim Author: Lukas Reineke -Version: 2.20.8 ============================================================================== -CONTENTS *indent-blankline* +CONTENTS *ibl* *indent-blankline* - 1. Introduction |indent-blankline-introduction| - 2. Highlights |indent-blankline-highlights| - 3. Setup |indent-blankline-setup| - 4. Variables |indent-blankline-variables| - 5. Commands |indent-blankline-commands| - 6. License |indent-blankline-license| + 1. Introduction |ibl.introduction| + 2. Functions |ibl.functions| + 3. Types |ibl.types| + 4. Commands |ibl.commands| + 5. License |ibl.license| ============================================================================== - 1. INTRODUCTION *indent-blankline-introduction* + 1. INTRODUCTION *ibl.introduction* -This plugin adds indentation guides to all lines (including empty lines). + This plugin adds indentation guides to Neovim. + It uses Neovim's virtual text feature and **no conceal** -It uses Neovim's virtual text feature and **no conceal** + To start using indent-blankline, call the |ibl.setup()| function. -This plugin requires Neovim 0.5 or higher. It makes use of Neovim only -features so it will not work in Vim. -There is a legacy version of the plugin that supports Neovim 0.4 under the -branch `version-1` + This plugin requires the latest stable version of Neovim. ============================================================================== - 2. HIGHLIGHTS *indent-blankline-highlights* + 2. FUNCTIONS *ibl.functions* + +setup({config}) *ibl.setup()* -To change the highlighting of either the indent character, or the whitespace -between indent characters, define or update these highlight groups. + Initializes and configures indent-blankline. ------------------------------------------------------------------------------- -IndentBlanklineChar *hl-IndentBlanklineChar* + Optionally, the first parameter can be a configuration table. + All values that are not passed in the table are set to the default value. + List values get merged with the default list value. - Highlight of indent character. + `setup` is idempotent, meaning you can call it multiple times, and each call + will reset indent-blankline. If you want to only update the current + configuration, use |ibl.update()|. - Default: takes guifg color from 'Whitespace' ~ + Parameters: ~ + • {config} (|ibl.config|?) Configuration table - Example: > + Example: ~ + >lua + require "ibl".setup() - highlight IndentBlanklineChar guifg=#00FF00 gui=nocombine ------------------------------------------------------------------------------- -IndentBlanklineSpaceChar *hl-IndentBlanklineSpaceChar* +update({config}) *ibl.update()* - Highlight of space character. + Updates the indent-blankline configuration - Default: takes guifg color from 'Whitespace' ~ + The first parameter is a configuration table. + All values that are not passed in the table are kept as they are. + List values get merged with the current list value. - Example: > + Parameters: ~ + • {config} (|ibl.config|) Configuration table - highlight IndentBlanklineSpaceChar guifg=#00FF00 gui=nocombine + Example: ~ + >lua + require "ibl".update { enabled = false } +< ------------------------------------------------------------------------------- -IndentBlanklineSpaceCharBlankline *hl-IndentBlanklineSpaceCharBlankline* +overwrite({config}) *ibl.overwrite()* - Highlight of space character on blank lines. + Overwrites the indent-blankline configuration - Default: takes guifg color from 'Whitespace' ~ + The first parameter is a configuration table. + All values that are not passed in the table are kept as they are. + All values that are passed overwrite existing and default values. - Example: > + Parameters: ~ + • {config} (|ibl.config|) Configuration table - highlight IndentBlanklineSpaceCharBlankline guifg=#00FF00 gui=nocombine + Example: ~ + >lua + require "ibl".overwrite { + exclude = { filetypes = {} } + } +< ------------------------------------------------------------------------------- -IndentBlanklineContextChar *hl-IndentBlanklineContextChar* +setup_buffer({bufnr}, {config}) *ibl.setup_buffer()* - Highlight of indent character when base of current context. - Only used when |g:indent_blankline_show_current_context| is active + Configures indent-blankline for one buffer - Default: takes guifg color from 'Label' ~ + All values that are not passed are cleared, and will fall back to + the global config. + List values get merged with the global config values. - Example: > + Parameters: ~ + • {bufnr} (number) Buffer number (0 for current buffer) + • {config} (|ibl.config|?) Configuration table - highlight IndentBlanklineContextChar guifg=#00FF00 gui=nocombine ------------------------------------------------------------------------------- -IndentBlanklineContextSpaceChar *hl-IndentBlanklineContextSpaceChar* +refresh({bufnr}) *ibl.refresh()* - Highlight of space characters one indent level of the current context. - Only used when |g:indent_blankline_show_current_context| is active + Refreshes indent-blankline in one buffer - Default: takes guifg color from 'Label' ~ + Only use this directly if you know what you are doing, + consider |ibl.debounced_refresh| instead - Example: > + Parameters: ~ + • {bufnr} (number) Buffer number (0 for current buffer) - highlight IndentBlanklineContextSpaceChar guifg=#00FF00 gui=nocombine ------------------------------------------------------------------------------- -IndentBlanklineContextStart *hl-IndentBlanklineContextStart* +debounced_refresh({bufnr}) *ibl.debounced_refresh()* - Highlight of the first line of the current context. - Only used when |g:indent_blankline_show_current_context_start| is active + Refreshes indent-blankline in one buffer, debounced - Default: takes guifg color from 'Label' as guisp and adds underline ~ + Parameters: ~ + • {bufnr} (number) Buffer number (0 for current buffer) - Note: You need to have set |gui-colors| for the default to work. - Example: > +refresh_all() *ibl.refresh_all()* - highlight IndentBlanklineContextStart guisp=#00FF00 gui=underline + Refreshes indent-blankline in all buffers ------------------------------------------------------------------------------- -Note: Define your highlight group after setting colorscheme or your colorscheme will clear your highlight group +hooks.register({type}, {fn}, {opts}) *ibl.hooks.register()* -When defining the highlight group, it is important to set |nocombine| as a -gui option. This is to make sure the character does not inherit gui options -from the underlying text, like italic or bold. + Registers a hook. + See |ibl.hooks| for more information -Highlight groups get reset on |ColorScheme| autocommand, if both fg and bg -are empty. + Each hook type takes a different callback, and a configuration table -The set more than one highlight group that changes based on indentation level, -see: -|g:indent_blankline_char_highlight_list| -|g:indent_blankline_space_char_highlight_list| -|g:indent_blankline_space_char_blankline_highlight_list| + Parameters: ~ + • {type} (|ibl.hooks.type|) Type of the hook + • {cb} (|ibl.hooks.cb|) Callback function + • {opts} (|ibl.hooks.options|?) Optional options for the hook -============================================================================== - 3. SETUP *indent-blankline-setup* - -To configure indent-blankline, either run the setup function, or set variables -manually. -The setup function has a single table as argument, keys of the table match the -|indent-blankline-variables| without the `indent_blankline_` part. - -Example: > - - require("indent_blankline").setup { - -- for example, context is off by default, use this to turn it on - show_current_context = true, - show_current_context_start = true, - } - -============================================================================== - 4. VARIABLES *indent-blankline-variables* - -All variables can be set globally |g:var|, per tab |t:var|, or per buffer |b:var| - ------------------------------------------------------------------------------- -g:indent_blankline_char *g:indent_blankline_char* - - Specifies the character to be used as indent line. - Not used if |g:indent_blankline_char_list| is not empty. - - When set explicitly to empty string (""), no indentation character is - displayed at all, even when |g:indent_blankline_char_list| is not empty. - This can be useful in combination with - |g:indent_blankline_space_char_highlight_list| to only rely on different - highlighting of different indentation levels without needing to show a - special character. - - Also set by |g:indentLine_char| - - Default: '│' ~ - - Example: > - - let g:indent_blankline_char = '|' - ------------------------------------------------------------------------------- -g:indent_blankline_char_blankline *g:indent_blankline_char_blankline* - - Specifies the character to be used as indent line for blanklines. - Not used if |g:indent_blankline_char_list_blankline| is not empty. - - Default: '' ~ - - Example: > - - let g:indent_blankline_char_blankline = '┆' - ------------------------------------------------------------------------------- -g:indent_blankline_char_list *g:indent_blankline_char_list* - - Specifies a list of characters to be used as indent line for - each indentation level. - Ignored if the value is an empty list. - - Also set by |g:indentLine_char_list| - - Default: [] ~ - - Example: > - - let g:indent_blankline_char_list = ['|', '¦', '┆', '┊'] - ------------------------------------------------------------------------------- -g:indent_blankline_char_list_blankline *g:indent_blankline_char_list_blankline* - - Specifies a list of characters to be used as indent line for - each indentation level on blanklines. - Ignored if the value is an empty list. - - Default: [] ~ - - Example: > - - let g:indent_blankline_char_list_blankline = ['|', '¦', '┆', '┊'] - ------------------------------------------------------------------------------- -g:indent_blankline_char_highlight_list *g:indent_blankline_char_highlight_list* - - Specifies the list of character highlights for each indentation level. - Ignored if the value is an empty list. - - Default: [] ~ + Return: ~ + (string) ID of the hook - Example: > + Example: ~ + >lua + local hooks = require "ibl.hooks" + hooks.register( + hooks.type.ACTIVE, + function(bufnr) + return vim.api.nvim_buf_line_count(bufnr) < 5000 + end + ) +< - let g:indent_blankline_char_highlight_list = ['Error', 'Function'] +hooks.clear({id}) *ibl.hooks.clear()* ------------------------------------------------------------------------------- -g:indent_blankline_space_char_blankline *g:indent_blankline_space_char_blankline* + Clears a hook by id - Specifies the character to be used as the space value in between indent - lines when the line is blank. + Parameters: ~ + • {id} (string) ID of the hook - Default: An empty space character ~ - Example: > +hooks.clear_all() *ibl.hooks.clear_all()* - let g:indent_blankline_space_char_blankline = ' ' + Clears all hooks ------------------------------------------------------------------------------- -g:indent_blankline_space_char_highlight_list *g:indent_blankline_space_char_highlight_list* - Specifies the list of space character highlights for each indentation - level. - Ignored if the value is an empty list. +hooks.get({bufnr}, {type}) *ibl.hooks.get()* - Default: [] ~ + Returns a list of all hooks for that buffer with the type - Example: > + Parameters: ~ + • {bufnr} (number) Buffer number (0 for current buffer) + • {type} (|ibl.hooks.type|) Type of the hook - let g:indent_blankline_space_char_highlight_list = ['Error', 'Function'] + Return: ~ + (|ibl.hooks.cb|[]) List of hooks ------------------------------------------------------------------------------- -g:indent_blankline_space_char_blankline_highlight_list *g:indent_blankline_space_char_blankline_highlight_list* - - Specifies the list of space character highlights for each indentation - level when the line is empty. - Ignored if the value is an empty list. - - Also set by |g:indent_blankline_space_char_highlight_list| - - Default: [] ~ - - Example: > - - let g:indent_blankline_space_char_blankline_highlight_list = ['Error', 'Function'] - ------------------------------------------------------------------------------- -g:indent_blankline_use_treesitter *g:indent_blankline_use_treesitter* - - Use treesitter to calculate indentation when possible. - Requires treesitter - - Default: false ~ - - Example: > - - let g:indent_blankline_use_treesitter = v:true - ------------------------------------------------------------------------------- -g:indent_blankline_indent_level *g:indent_blankline_indent_level* - - Specifies the maximum indent level to display. - - Also set by |g:indentLine_indentLevel| - - Default: 10 ~ - - Example: > - - let g:indent_blankline_indent_level = 4 - ------------------------------------------------------------------------------- -g:indent_blankline_max_indent_increase *g:indent_blankline_max_indent_increase* - - The maximum indent level increase from line to line. - Set this option to 1 to make aligned trailing multiline comments not - create indentation. - - Default: g:indent_blankline_indent_level ~ - - Example: > - - let g:indent_blankline_max_indent_increase = 1 - ------------------------------------------------------------------------------- -g:indent_blankline_show_first_indent_level *g:indent_blankline_show_first_indent_level* - - Displays indentation in the first column. +============================================================================== + 3. TYPES *ibl.types* - Default: v:true ~ +config *ibl.config* - Example: > + Configuration table for indent-blankline. - let g:indent_blankline_show_first_indent_level = v:false + Fields: ~ + *ibl.config.enabled* + • {enabled} (boolean) + Enables or disables indent-blankline ------------------------------------------------------------------------------- -g:indent_blankline_show_trailing_blankline_indent *g:indent_blankline_show_trailing_blankline_indent* + Default: `true` ~ - Displays a trailing indentation guide on blank lines, to match the - indentation of surrounding code. - Turn this off if you want to use background highlighting instead of chars. + *ibl.config.debounce* + • {debounce} (number) + Sets the amount indent-blankline debounces + refreshes in milliseconds - Default: v:true ~ + Default: `200` ~ - Example: > + • {viewport_buffer} (|ibl.config.viewport_buffer|) + Configures the viewport of where indentation guides + are generated + + • {indent} (|ibl.config.indent|) + Configures the indentation - let g:indent_blankline_show_trailing_blankline_indent = v:false + • {whitespace} (|ibl.config.whitespace|) + Configures the whitespace ------------------------------------------------------------------------------- -g:indent_blankline_show_end_of_line *g:indent_blankline_show_end_of_line* + • {scope} (|ibl.config.scope|) + Configures the scope + + • {exclude} (|ibl.config.exclude|) + Configures what is excluded from indent-blankline + + Example: ~ + >lua + { + debounce = 100, + indent = { char = "|" }, + whitespace = { highlight = { "Whitespace", "NonText" } }, + scope = { exclude = { "lua" } }, + } +< + +config.viewport_buffer *ibl.config.viewport_buffer* + + Configures the viewport of where indentation guides are generated + + Fields: ~ + *ibl.config.viewport_buffer.min* + • {min} (number) + Minimum number of lines above and below of what is currently + visible in the window for which indentation guides will + be generated + + Default: `30` ~ - Displays the end of line character set by |listchars| instead of the - indent guide on line returns. + *ibl.config.viewport_buffer.max* + • {max} (number) + Maximum number of lines above and below of what is currently + visible in the window for which indentation guides will + be generated + + Default: `500` ~ + + Example: ~ + >lua + { min = 100, max = 600 } +< - Default: v:false ~ +config.indent *ibl.config.indent* - Example: > + Configures the indentation - let g:indent_blankline_show_end_of_line = v:true + Fields: ~ + *ibl.config.indent.char* + • {char} (string|string[]) + Character, or list of characters, that get used to + display the indentation guide + Each character has to have a display width of 0 or 1 ------------------------------------------------------------------------------- -g:indent_blankline_show_foldtext *g:indent_blankline_show_foldtext* + Default: `▎` ~ - Displays the full fold text instead of the indent guide on folded lines. + *ibl.config.indent.tab_char* + • {tab_char} (string|string[]) + Character, or list of characters, that get used to + display the indentation guide for tabs + Each character has to have a display width of 0 or 1 - Note: there is no autocommand to subscribe to changes in folding. This - might lead to unexpected results. A possible solution for this is to - remap folding bindings to also call |IndentBlanklineRefresh| + Default: uses |lcs-tab| if |list| is set, ~ + otherwhise, uses |ibl.config.indent.char| ~ - Default: v:true ~ + *ibl.config.indent.highlight* + • {highlight} (string|string[]) + Highlight group, or list of highlight groups, that + get applied to the indentation guide - Example: > + Default: |hl-Whitespace| ~ - let g:indent_blankline_show_foldtext = v:false + *ibl.config.indent.smart_indent_cap* + • {smart_indent_cap} (boolean) + Caps the number of indentation levels by looking at + the surrounding code ------------------------------------------------------------------------------- -g:indent_blankline_enabled *g:indent_blankline_enabled* + Default: `true` ~ - Turns this plugin on or off. + Example: ~ + >c + # OFF + { + ▎ foo_bar(a, b, + ▎ ▎ ▎ ▎ ▎ c, d); + } - Also set by |g:indentLine_enabled| + # ON + { + ▎ foo_bar(a, b, + ▎ ▎ c, d); + } +< + *ibl.config.indent.priority* + • {priority} (number) + Virtual text priority for the indentation guide - Note: the buffer version of this variable overwrites all other - enabled/disabled checks. + Default: `1` ~ - Default: v:true ~ + Example: ~ + >lua + { + char = "|", + tab_char = { "a", "b", "c" }, + highlight = { "Function", "Label" }, + smart_indent_cap = true, + priority = 2, + } +< - Example: > +config.whitespace *ibl.config.whitespace* - let g:indent_blankline_enabled = v:false + Configures the whitespace ------------------------------------------------------------------------------- -g:indent_blankline_disable_with_nolist *g:indent_blankline_disable_with_nolist* + Fields: ~ + *ibl.config.whitespace.highlight* + • {highlight} (string|string[]) + Highlight group, or list of highlight groups, + that get applied to the whitespace + + Default: |hl-Whitespace| ~ + + *ibl.config.whitespace.remove_blankline_trail* + • {remove_blankline_trail} (boolean) + Removes trailing whitespace on blanklines - When true, automatically turns this plugin off when |nolist| is set. - When false, setting |nolist| will keep displaying indentation guides but - removes whitespace characters set by |listchars|. + Turn this off if you want to add background + color to the whitespace highlight group - Default: v:false ~ + Default: `true` ~ - Example: > + Example: ~ + >lua + { + highlight = { "Function", "Label" }, + remove_blankline_trail = true, + } +< - let g:indent_blankline_disable_with_nolist = v:true +config.scope *ibl.config.scope* ------------------------------------------------------------------------------- -g:indent_blankline_filetype *g:indent_blankline_filetype* + Configures the scope - Specifies a list of |filetype| values for which this plugin is enabled. - All |filetypes| are enabled if the value is an empty list. + The scope is *not* the current indentation level! Instead, it is the + indentation level where variables or functions are accessible. This depends + on the language you are writing. - Also set by |g:indentLine_fileType| + Note: Scope requires treesitter to be set up ~ - Default: [] ~ + Fields: ~ + *ibl.config.scope.enabled* + • {enabled} (boolean) + Enables or disables scope - Example: > + Default: `true` ~ - let g:indent_blankline_filetype = ['vim'] + *ibl.config.scope.char* + • {char} (string|string[]) + Character, or list of characters, that get used to + display the scope indentation guide + Each character has to have a display width + of 0 or 1 ------------------------------------------------------------------------------- -g:indent_blankline_filetype_exclude *g:indent_blankline_filetype_exclude* + Default: |ibl.config.indent.char| ~ - Specifies a list of |filetype| values for which this plugin is not enabled. - Ignored if the value is an empty list. + *ibl.config.scope.show_start* + • {show_start} (boolean) + Shows an underline on the first line of the scope - Also set by |g:indentLine_fileTypeExclude| + Default: `true` ~ - Default: [ ~ - "lspinfo", ~ - "packer", ~ - "checkhealth", ~ - "help", ~ - "man", ~ - "", ~ - ] ~ + *ibl.config.scope.show_end* + • {show_end} (boolean) + Shows an underline on the last line of the scope - Example: > + Default: `true` ~ - let g:indent_blankline_filetype_exclude = ['help'] + *ibl.config.scope.injected_languages* + • {injected_languages} (boolean) + Checks for the current scope in injected + treesitter languages ------------------------------------------------------------------------------- -g:indent_blankline_buftype_exclude *g:indent_blankline_buftype_exclude* + This also influences if the scope gets excluded + or not - Specifies a list of |buftype| values for which this plugin is not enabled. - Ignored if the value is an empty list. + Default: `true` ~ - Also set by |g:indentLine_bufTypeExclude| + *ibl.config.scope.highlight* + • {highlight} (string|string[]) + Highlight group, or list of highlight groups, + that get applied to the scope - Default: [ ~ - "terminal", ~ - "nofile", ~ - "quickfix", ~ - "prompt", ~ - ] ~ + Default: |hl-LineNr| ~ - Example: > + *ibl.config.scope.priority* + • {priority} (number) + Virtual text priority for the scope - let g:indent_blankline_buftype_exclude = ['terminal'] + Default: `1024` ~ ------------------------------------------------------------------------------- -g:indent_blankline_bufname_exclude *g:indent_blankline_bufname_exclude* + • {include} (|ibl.config.scope.include|) + Configures additional nodes to be used as scope - Specifies a list of buffer names (file name with full path) for which - this plugin is not enabled. - A name can be regular expression as well. + • {exclude} (|ibl.config.scope.exclude|) + Configures nodes or languages to be excluded + from scope - Also set by |g:indentLine_bufNameExclude| + Example: ~ + >lua + { + enabled = true, + show_start = true, + show_end = false, + injected_languages = false, + highlight = { "Function", "Label" }, + priority = 500, + } +< - Default: [] ~ +config.scope.include *ibl.config.scope.include* - Example: > + Configures additional nodes to be used as scope - let g:indent_blankline_bufname_exclude = ['README.md', '.*\.py'] + Fields: ~ + *ibl.config.scope.include.node_type* + • {node_type} (table) + map of language to a list of node types which can be + used as scope ------------------------------------------------------------------------------- -g:indent_blankline_strict_tabs *g:indent_blankline_strict_tabs* + Use `*` as a wildcard for all languages - When on, if there is a single tab in a line, only tabs are used to - calculate the indentation level. - When off, both spaces and tabs are used to calculate the indentation - level. - Only makes a difference if a line has a mix of tabs and spaces for - indentation. + Default: empty ~ - Default: v:false ~ + Example: ~ + >lua + { + node_type = { lua = { "return_statement", "table_constructor" } }, + } - Example: > - let g:indent_blankline_strict_tabs = v:true +config.scope.exclude *ibl.config.scope.exclude* ------------------------------------------------------------------------------- -g:indent_blankline_show_current_context *g:indent_blankline_show_current_context* + Configures nodes or languages to be excluded from scope - When on, use treesitter to determine the current context. Then show the - indent character in a different highlight. + Fields: ~ + *ibl.config.scope.exclude.language* + • {language} (string[]) + List of treesitter languages for which scope is disabled - Note: Requires https://github.com/nvim-treesitter/nvim-treesitter to be - installed + Default: empty ~ - Note: With this option enabled, the plugin refreshes on |CursorMoved|, - which might be slower + *ibl.config.scope.exclude.node_type* + • {node_type} (table) + map of language to a list of node types which should not + be used as scope - Default: v:false ~ + Use `*` as a wildcard for all languages - Example: > + Default: ~ + • `*`: + • `source_file` + • `program` + • `lua`: + • `chunk` + • `python`: + • `module` - let g:indent_blankline_show_current_context = v:true + Example: ~ + >lua + { + language = { "rust" }, + node_type = { lua = { "block", "chunk" } }, + } +< ------------------------------------------------------------------------------- -g:indent_blankline_show_current_context_start *g:indent_blankline_show_current_context_start* +config.exclude *ibl.config.exclude* - Applies the |hl-IndentBlanklineContextStart| highlight group to the first - line of the current context. - By default this will underline. + Configures what is excluded from indent-blankline - Note: Requires https://github.com/nvim-treesitter/nvim-treesitter to be - installed + Fields: ~ + *ibl.config.exclude.filetypes* + • {filetypes} (string[]) + List of |'filetype'|s for which indent-blankline is disabled - Note: You need to have set |gui-colors| and it depends on your terminal - emulator if this works as expected. - If you are using kitty and tmux, take a look at this article to - make it work - http://evantravers.com/articles/2021/02/05/curly-underlines-in-kitty-tmux-neovim/ + Default: ~ + • `lspinfo` + • `packer` + • `checkhealth` + • `help` + • `man` + • `gitcommit` + • `TelescopePrompt` + • `TelescopeResults` + • `''` - Default: v:false ~ + *ibl.config.exclude.buftypes* + • {buftypes} (string[]) + List of |'buftype'|s for which indent-blankline is disabled - Example: > + Default: ~ + • `terminal` + • `nofile` + • `quickfix` + • `prompt` - let g:indent_blankline_show_current_context_start = v:true + Example: ~ + >lua + { + filetypes = { "rust" }, + buftypes = { "terminal" }, + } +< ------------------------------------------------------------------------------- -g:indent_blankline_show_current_context_start_on_current_line *g:indent_blankline_show_current_context_start_on_current_line* +indent.whitespace *ibl.indent.whitespace* - Shows |g:indent_blankline_show_current_context_start| even when the cursor - is on the same line + Enum of whitespace types - Default: v:true ~ + Variants: ~ + • {TAB_START} + • {TAB_START_SINGLE} + • {TAB_FILL} + • {TAB_END} + • {SPACE} + • {INDENT} - Example: > - let g:indent_blankline_show_current_context_start_on_current_line = v:false +hooks *ibl.hooks* ------------------------------------------------------------------------------- -g:indent_blankline_context_char *g:indent_blankline_context_char* + Hooks provide a way to extend the functionality of indent-blankline. Either + from your own config, or even from other third part plugins. + Hooks consist of a type (|ibl.hooks.type|) and a callback + function (|ibl.hooks.cb|). When indent-blankline computes values for which + hooks exist, for example if a buffer is active, it then calls all registered + hooks for that type to get the final value. - Specifies the character to be used for the current context indent line. - Not used if |g:indent_blankline_context_char_list| is not empty. + Most hooks can be global or buffer scoped. - Useful to have a greater distinction between the current context indent - line and others. - Also useful in combination with |g:indent_blankline_char| set to empty string - (""), as this allows only the current context indent line to be shown. +hooks.type *ibl.hooks.type* - Default: g:indent_blankline_char ~ + Enum of hook types - Example: > + Variants: ~ + • {ACTIVE} + • {SCOPE_ACTIVE} + • {SKIP_LINE} + • {WHITESPACE} + • {VIRTUAL_TEXT} + • {SCOPE_HIGHLIGHT} + • {CLEAR} + • {HIGHLIGHT_SETUP} - let g:indent_blankline_context_char = '┃' ------------------------------------------------------------------------------- -g:indent_blankline_context_char_blankline *g:indent_blankline_context_char_blankline* +hooks.cb *ibl.hooks.cb* - Equivalent of |g:indent_blankline_char_blankline| for - |g:indent_blankline_context_char|. + Each hook type takes a different callback function - Default: '' ~ - Example: > +hooks.cb.active({bufnr}) *ibl.hooks.cb.active()* - let g:indent_blankline_context_char_blankline = '┆' + Callback function for the |ibl.hooks.type|.ACTIVE hook. ------------------------------------------------------------------------------- -g:indent_blankline_context_char_list *g:indent_blankline_context_char_list* + Gets called before refreshing indent-blankline for a buffer. + If the callback returns false, the buffer will not be refreshed, and all + existing indentation guides will be cleared. - Equivalent of |g:indent_blankline_char_list| for - |g:indent_blankline_context_char|. + Parameters: ~ + • {bufnr} (number) Buffer number - Default: [] ~ + Return: ~ + (boolean) - Example: > - let g:indent_blankline_context_char_list = ['┃', '║', '╬', '█'] +hooks.cb.scope_active({bufnr}) *ibl.hooks.cb.scope_active()* ------------------------------------------------------------------------------- -g:indent_blankline_context_char_list_blankline *g:indent_blankline_context_char_list_blankline* + Callback function for the |ibl.hooks.type|.SCOPE_ACTIVE hook. - Equivalent of |g:indent_blankline_char_list_blankline| for - |g:indent_blankline_context_char_blankline|. + Gets called before refreshing indent-blankline for a buffer. + If the callback returns false, |ibl.config.scope| will be disabled. - Default: [] ~ + Parameters: ~ + • {bufnr} (number) Buffer number - Example: > + Return: ~ + (boolean) - let g:indent_blankline_context_char_list_blankline = ['┃', '║', '╬', '█'] ------------------------------------------------------------------------------- -g:indent_blankline_context_highlight_list *g:indent_blankline_context_highlight_list* +hooks.cb.skip_line({tick}, {bufnr}, {row}, {line}) *ibl.hooks.cb.skip_line()* - Specifies the list of character highlights for the current context at - each indentation level. - Ignored if the value is an empty list. + Callback function for the |ibl.hooks.type|.SKIP_LINE hook. - Only used when |g:indent_blankline_show_current_context| is active + Gets called for every line before indentation is calculated. + If the callback returns true, the line will get skipped. - Default: [] ~ + Parameters: ~ + • {tick} (number) auto-incrementing id of the current refresh + • {bufnr} (number) Buffer number + • {row} (number) Row of the buffer + • {line} (string) Text of that row - Example: > + Return: ~ + (boolean) - let g:indent_blankline_context_highlight_list = ['Error', 'Warning'] + *ibl.hooks.cb.whitespace()* +hooks.cb.whitespace({tick}, {bufnr}, {row}, {whitespace}) ------------------------------------------------------------------------------- -g:indent_blankline_char_priority *g:indent_blankline_char_priority* + Callback function for the |ibl.hooks.type|.WHITESPACE hook. - Specifies the |extmarks| priority for chars. + Gets called for every line after the whitespace is determined. + The return value overwrites the whitespace for that line. - Default: 1 ~ + Whitespace is a table of `ibl.indent.whitespace` enum values, where each + value represents a display cell. - Example: > + Parameters: ~ + • {tick} (number) auto-incrementing id of the current refresh + • {bufnr} (number) Buffer number + • {row} (number) Row of the buffer + • {whitespace} (|ib.indent.whitespace|[]) List of whitespace enum - let g:indent_blankline_char_priority = 50 + Return: ~ + (|ib.indent.whitespace|[]) ------------------------------------------------------------------------------- -g:indent_blankline_context_start_priority *g:indent_blankline_context_start_priority* + *ibl.hooks.cb.virtual_text()* +hooks.cb.virtual_text({tick}, {bufnr}, {row}, {virtual_text}) - Specifies the |extmarks| priority for the context start. + Callback function for the |ibl.hooks.type|.VIRTUAL_TEXT hook. - Default: 10000 ~ + Gets called for every line after the virtual text is determened. + The return value overwrites the virtual text for that line. - Example: > + See |nvim_buf_set_extmark()| for more information about virtual text. - let g:indent_blankline_context_start_priority = 50 + Parameters: ~ + • {tick} (number) auto-incrementing id of the current refresh + • {bufnr} (number) Buffer number + • {row} (number) Row of the buffer + • {virtual_text} ({string, string|string[]}[]) Virtual text for the line ------------------------------------------------------------------------------- -g:indent_blankline_context_patterns *g:indent_blankline_context_patterns* + Return: ~ + ({string, string|string[]}[]) - Specifies a list of lua patterns that are used to match against the - treesitter |tsnode:type()| at the cursor position to find the current - context. + *ibl.hooks.cb.scope_highlight()* +hooks.cb.scope_highlight({tick}, {bufnr}, {scope}, {scope_index}) - To learn more about how lua pattern work, see here: - https://www.lua.org/manual/5.1/manual.html#5.4.1 + Callback function for the |ibl.hooks.type|.SCOPE_HIGHLIGHT hook. - Only used when |g:indent_blankline_show_current_context| is active + Gets called for once per refresh after the scope is determined. + The return value overwrites the index of the highlight group + defined in |ibl.config.scope.highlight| - Default: [ ~ - "class", ~ - "^func", ~ - "method", ~ - "^if", ~ - "while", ~ - "for", ~ - "with", ~ - "try", ~ - "except", ~ - "arguments", ~ - "argument_list", ~ - "object", ~ - "dictionary", ~ - "element", ~ - "table", ~ - "tuple", ~ - "do_block", ~ - "Block", ~ - "InitList", ~ - "FnCallArguments", ~ - "IfStatement", ~ - "ContainerDecl", ~ - "SwitchExpr", ~ - "IfExpr", ~ - "ParamDeclList", ~ - ] ~ + Parameters: ~ + • {tick} (number) auto-incrementing id of the current refresh + • {bufnr} (number) Buffer number + • {scope} (|TSNode|) The current scope + • {scope_index} (number) Index of the highlight group - Example: > + Return: ~ + (number) - let g:indent_blankline_context_patterns = ['^if'] ------------------------------------------------------------------------------- -g:indent_blankline_use_treesitter_scope *g:indent_blankline_use_treesitter_scope* +hooks.cb.clear({bufnr}) *ibl.hooks.cb.clear()* - Instead of using |g:indent_blankline_context_patterns|, use the current - scope defined by nvim-treesitter as the context. + Callback function for the |ibl.hooks.type|.CLEAR hook. - Default: false ~ + Gets called when a buffer is cleared. - Example: > + Parameters: ~ + • {bufnr} (number) Buffer number - let g:indent_blankline_use_treesitter_scope = true ------------------------------------------------------------------------------- -g:indent_blankline_context_pattern_highlight *g:indent_blankline_context_pattern_highlight* +hooks.cb.highlight_setup() *ibl.hooks.cb.highlight_setup()* - Specifies a map of patterns set in - |g:indent_blankline_context_patterns| to highlight groups. - When the current matching context pattern is in the map, the context - will be highlighted with the corresponding highlight group. + Callback function for the |ibl.hooks.type|.HIGHLIGHT_SETUP hook. - Only used when |g:indent_blankline_show_current_context| is active + Gets called before the highlight groups are created. + If you want to define custom highlight groups, do it in this hook, so they + are reset if you change the colorscheme with |:colorscheme|. - Default: {} ~ - Example: > +hooks.options *ibl.hooks.options* - let g:indent_blankline_context_pattern_highlight = {'function': 'Function'} + Option table for hook registration ------------------------------------------------------------------------------- -g:indent_blankline_viewport_buffer *g:indent_blankline_viewport_buffer* + Fields: ~ + *ibl.hooks.options.bufnr* + • {bufnr} (number) + Buffer number (0 for current buffer) - Sets the buffer of extra lines before and after the current viewport that - are considered when generating indentation and the context. - Default: 10 ~ +hooks.builtin *ibl.hooks.builtin* - Example: > +hooks.builtin.skip_preproc_lines *ibl.hooks.builtin.skip_preproc_lines* - let g:indent_blankline_viewport_buffer = 20 + Skip preproc lines + Automatically active for `c` and `cpp` ------------------------------------------------------------------------------- -g:indent_blankline_disable_warning_message *g:indent_blankline_disable_warning_message* + Example: ~ + >lua + local hooks = require "ibl.hooks" + hooks.register( + hooks.type.SKIP_LINE, + hooks.builtin.skip_preproc_lines, + { bufnr = 0 } + ) +< + *hooks.builtin.scope_highlight_from_extmark* +hooks.builtin.scope_highlight_from_extmark - Turns deprecation warning messages off. + Gets the highlight group from existing extmark highlights at the end or + beginning of the scope. + This can be used to get a somewhat reliable sync between + "rainbow parentheses" plugins like + https://gitlab.com/HiPhish/rainbow-delimiters.nvim and indent-blankline. - Default: v:false ~ + Example: ~ + >lua + local highlight = { + "RainbowDelimiterRed", + "RainbowDelimiterYellow", + "RainbowDelimiterBlue", + "RainbowDelimiterOrange", + "RainbowDelimiterGreen", + "RainbowDelimiterViolet", + "RainbowDelimiterCyan", + } + vim.g.rainbow_delimiters = { highlight = highlight } + require "ibl".setup { scope = { highlight = highlight } } - Example: > + local hooks = require "ibl.hooks" + hooks.register( + hooks.type.SCOPE_HIGHLIGHT, + hooks.builtin.scope_highlight_from_extmark + ) +< + *ibl.hooks.builtin.hide_first_space_indent_level* +hooks.builtin.hide_first_space_indent_level - let g:indent_blankline_disable_warning_message = v:true + Replaces the first indentation guide for space indentation with a normal + space. + Example: ~ + >lua + local hooks = require "ibl.hooks" + hooks.register( + hooks.type.WHITESPACE, + hooks.builtin.hide_first_space_indent_level + ) +< + *ibl.hooks.builtin.hide_first_tab_indent_level* +hooks.builtin.hide_tab_space_indent_level + + Replaces the first indentation guide for tab indentation with + |lcs-tab| tab fill. + + Example: ~ + >lua + local hooks = require "ibl.hooks" + hooks.register( + hooks.type.WHITESPACE, + hooks.builtin.hide_first_tab_indent_level + ) +< ============================================================================== - 5. COMMANDS *indent-blankline-commands* - ------------------------------------------------------------------------------- -:IndentBlanklineRefresh[!] *IndentBlanklineRefresh* - - Refreshes the indent guides for the current buffer. - Run this with |autocmd| when the file changes. For example after a plugin - formats the file. - - With bang (IndentBlanklineRefresh!) refreshes the indent guides globally. - - By default it is run for: - 1. |FileChangedShellPost| * - 2. |TextChanged| * - 3. |TextChangedI| * - 4. |CompleteChanged| * - 5. |BufWinEnter| * - 6. |Filetype| * - 7. |OptionSet| list,listchars,shiftwidth,tabstop,expandtab - - Example: > - - autocmd User ALEFixPost IndentBlanklineRefresh - ------------------------------------------------------------------------------- -:IndentBlanklineRefreshScroll[!] *IndentBlanklineRefreshScroll* - - Refreshes the indent guides for the current buffer. But tries to reuse as - indent guides that already exist. Only used if we are sure the buffer - content did not change. - - With bang (IndentBlanklineRefresh!) refreshes the indent guides globally. + 4. COMMANDS *ibl.commands* - By default it is run for: - 1. |WinScrolled| * +:IBLEnable *:IBLEnable* - Example: > + Enables indent-blankline - autocmd WinScrolled * IndentBlanklineRefreshScroll +:IBLDisable *:IBLDisable* ------------------------------------------------------------------------------- -:IndentBlanklineEnable[!] *IndentBlanklineEnable* + Disables indent-blankline - Enables this plugin for the current buffer. - This overwrites any include/exclude rules. +:IBLToggle *:IBLToggle* - With bang (IndentBlanklineEnable!) enables this plugin globally + Toggles indent-blankline on and off ------------------------------------------------------------------------------- -:IndentBlanklineDisable[!] *IndentBlanklineDisable* +:IBLEnableScope *:IBLEnableScope* - Disables this plugin for the current buffer. - This overwrites any include/exclude rules. + Enables indent-blanklines scope - With bang (IndentBlanklineDisable!) disables this plugin globally +:IBLDisableScope *:IBLDisableScope* ------------------------------------------------------------------------------- -:IndentBlanklineToggle[!] *IndentBlanklineToggle* + Disables indent-blanklines scope - Toggles between |IndentBlanklineEnable| and |IndentBlanklineDisable|. +:IBLToggleScope *:IBLToggleScope* - With bang (IndentBlanklineToggle!) toggles globally + Toggles indent-blanklines scope on and off ============================================================================== - 6. LICENSE *indent-blankline-license* - -The MIT Licence -http://www.opensource.org/licenses/mit-license.php - -Copyright (c) 2023 Lukas Reineke - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + 5. LICENSE *ibl.license* + + The MIT Licence + http://www.opensource.org/licenses/mit-license.php + + Copyright (c) 2023 Lukas Reineke + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. ============================================================================== vim:tw=78:ts=8:ft=help:norl diff --git a/lua/ibl/autocmds.lua b/lua/ibl/autocmds.lua new file mode 100644 index 00000000..91c6950d --- /dev/null +++ b/lua/ibl/autocmds.lua @@ -0,0 +1,61 @@ +local highlights = require "ibl.highlights" +local M = {} + +M.setup = function() + local group = vim.api.nvim_create_augroup("IndentBlankline", {}) + local ibl = require "ibl" + local buffer_leftcol = {} + + vim.api.nvim_create_autocmd("VimEnter", { + group = group, + pattern = "*", + callback = ibl.refresh_all, + }) + vim.api.nvim_create_autocmd({ + "CursorMoved", + "BufWinEnter", + "CompleteChanged", + "FileChangedShellPost", + "FileType", + "TextChanged", + "TextChangedI", + }, { + group = group, + pattern = "*", + callback = function(opts) + ibl.debounced_refresh(opts.buf) + end, + }) + vim.api.nvim_create_autocmd("OptionSet", { + group = group, + pattern = "list,listchars,shiftwidth,tabstop,vartabstop", + callback = function(opts) + ibl.debounced_refresh(opts.buf) + end, + }) + vim.api.nvim_create_autocmd("WinScrolled", { + group = group, + pattern = "*", + callback = function(opts) + local win_view = vim.fn.winsaveview() or { leftcol = 0 } + + if buffer_leftcol[opts.buf] ~= win_view.leftcol then + buffer_leftcol[opts.buf] = win_view.leftcol + -- Refresh immediately for horizontal scrolling + ibl.refresh(opts.buf) + else + ibl.debounced_refresh(opts.buf) + end + end, + }) + vim.api.nvim_create_autocmd("ColorScheme", { + group = group, + pattern = "*", + callback = function() + highlights.setup() + ibl.refresh_all() + end, + }) +end + +return M diff --git a/lua/ibl/config.lua b/lua/ibl/config.lua new file mode 100644 index 00000000..dfafa3b1 --- /dev/null +++ b/lua/ibl/config.lua @@ -0,0 +1,328 @@ +local utils = require "ibl.utils" + +local M = {} + +--- The current global configuration +--- +---@type ibl.config.full? +M.config = nil + +--- Map from buffer numbers to their partial configuration +--- +--- Anything not included here will fall back to the global configuration +---@type table +M.buffer_config = {} + +--- The default configuration +--- +---@type ibl.config.full +M.default_config = { + enabled = true, + debounce = 200, + viewport_buffer = { + min = 30, + max = 500, + }, + indent = { + char = "▎", + tab_char = nil, + highlight = "Whitespace", + smart_indent_cap = true, + priority = 1, + }, + whitespace = { + highlight = "Whitespace", + remove_blankline_trail = true, + }, + scope = { + enabled = true, + char = nil, + show_start = true, + show_end = true, + injected_languages = true, + highlight = "LineNr", + priority = 1024, + include = { + node_type = {}, + }, + exclude = { + language = {}, + node_type = { + ["*"] = { + "source_file", + "program", + }, + lua = { + "chunk", + }, + python = { + "module", + }, + }, + }, + }, + exclude = { + filetypes = { + "lspinfo", + "packer", + "checkhealth", + "help", + "man", + "gitcommit", + "TelescopePrompt", + "TelescopeResults", + "", + }, + buftypes = { + "terminal", + "nofile", + "quickfix", + "prompt", + }, + }, +} + +---@param char string +---@return boolean, string? +local validate_char = function(char) + if type(char) == "string" then + local length = vim.fn.strdisplaywidth(char) + return length <= 1, string.format("'%s' has a dispaly width of %d", char, length) + else + if #char == 0 then + return false, "table is empty" + end + for i, c in ipairs(char) do + local length = vim.fn.strdisplaywidth(c) + if length > 1 then + return false, string.format("index %d '%s' has a display width of %d", i, c, length) + end + end + return true + end +end + +---@param config ibl.config? +local validate_config = function(config) + if not config then + return + end + + vim.validate { + enabled = { config.enabled, "boolean", true }, + viewport_buffer = { config.viewport_buffer, "table", true }, + indent = { config.indent, "table", true }, + whitespace = { config.whitespace, "table", true }, + scope = { config.scope, "table", true }, + exclude = { config.exclude, "table", true }, + } + + if config.viewport_buffer then + vim.validate { + min = { config.viewport_buffer.min, "number", true }, + max = { config.viewport_buffer.max, "number", true }, + } + end + + if config.indent then + vim.validate { + char = { config.indent.char, { "string", "table" }, true }, + highlight = { config.indent.highlight, { "string", "table" }, true }, + smart_indent_cap = { config.indent.smart_indent_cap, "boolean", true }, + priority = { config.indent.priority, "number", true }, + } + if config.indent.char then + vim.validate { + char = { + config.indent.char, + validate_char, + "indent.char to have a display width of 0 or 1", + }, + } + end + if config.indent.tab_char then + vim.validate { + tab_char = { + config.indent.tab_char, + validate_char, + "indent.tab_char to have a display width of 0 or 1", + }, + } + end + if type(config.indent.highlight) == "table" then + vim.validate { + tab_char = { + config.indent.highlight, + function(highlight) + return #highlight > 0 + end, + "indent.highlight to be not empty", + }, + } + end + end + + if config.whitespace then + vim.validate { + highlight = { config.whitespace.highlight, { "string", "table" }, true }, + remove_blankline_trail = { config.whitespace.remove_blankline_trail, "boolean", true }, + } + if type(config.whitespace.highlight) == "table" then + vim.validate { + tab_char = { + config.whitespace.highlight, + function(highlight) + return #highlight > 0 + end, + "whitespace.highlight to be not empty", + }, + } + end + end + + if config.scope then + vim.validate { + enabled = { config.scope.enabled, "boolean", true }, + show_start = { config.scope.show_start, "boolean", true }, + show_end = { config.scope.show_end, "boolean", true }, + injected_languages = { config.scope.injected_languages, "boolean", true }, + highlight = { config.scope.highlight, { "string", "table" }, true }, + priority = { config.scope.priority, "number", true }, + include = { config.scope.include, "table", true }, + exclude = { config.scope.exclude, "table", true }, + } + if config.scope.char then + vim.validate { + char = { + config.scope.char, + validate_char, + "scope.char to have a display width of 0 or 1", + }, + } + end + if type(config.scope.highlight) == "table" then + vim.validate { + tab_char = { + config.scope.highlight, + function(highlight) + return #highlight > 0 + end, + "scope.highlight to be not empty", + }, + } + end + if config.scope.exclude then + vim.validate { + language = { config.scope.exclude.language, "table", true }, + node_type = { config.scope.exclude.node_type, "table", true }, + } + end + if config.scope.include then + vim.validate { + node_type = { config.scope.include.node_type, "table", true }, + } + end + end + + if config.exclude then + if config.exclude then + vim.validate { + filetypes = { config.exclude.filetypes, "table", true }, + buftypes = { config.exclude.buftypes, "table", true }, + } + end + end +end + +---@param behavior "merge"|"overwrite" +---@param base ibl.config.full +---@param input ibl.config? +---@return ibl.config.full +local merge_configs = function(behavior, base, input) + local result = vim.tbl_deep_extend("keep", input or {}, base) --[[@as ibl.config.full]] + + if behavior == "merge" and input then + result.scope.exclude.language = + utils.tbl_join(base.scope.exclude.language, vim.tbl_get(input, "scope", "exclude", "language")) + + local node_type = vim.tbl_get(input, "scope", "exclude", "node_type") + if node_type then + for k, v in pairs(node_type) do + result.scope.exclude.node_type[k] = utils.tbl_join(v, base.scope.exclude.node_type[k]) + end + end + result.exclude.filetypes = utils.tbl_join(base.exclude.filetypes, vim.tbl_get(input, "exclude", "filetypes")) + result.exclude.buftypes = utils.tbl_join(base.exclude.buftypes, vim.tbl_get(input, "exclude", "buftypes")) + end + + return result +end + +--- Sets the global configuration +--- +--- All values that are not passed are set to the default value +--- List values get merged with the default values +---@param config ibl.config? +---@return ibl.config.full +M.set_config = function(config) + validate_config(config) + M.config = merge_configs("merge", M.default_config, config) + + return M.config +end + +--- Updates the global configuration +--- +--- All values that are not passed are kept as they are +---@param config ibl.config +---@return ibl.config.full +M.update_config = function(config) + validate_config(config) + M.config = merge_configs("merge", M.config or M.default_config, config or {}) + + return M.config +end + +--- Overwrites the global configuration +--- +--- Same as `set_config`, but all list values are overwritten instead of merged +---@param config ibl.config +---@return ibl.config.full +M.overwrite_config = function(config) + validate_config(config) + M.config = merge_configs("overwrite", M.default_config, config) + + return M.config +end + +--- Sets the configuration for a buffer +--- +--- All values that are not passed are cleared, and will fall back to the global config +---@param bufnr number +---@param config ibl.config +---@return ibl.config.full +M.set_buffer_config = function(bufnr, config) + validate_config(config) + bufnr = utils.get_bufnr(bufnr) + M.buffer_config[bufnr] = config + + return M.get_config(bufnr) +end + +--- Clears the configuration for a buffer +--- +---@param bufnr number +M.clear_buffer_config = function(bufnr) + M.buffer_config[bufnr] = nil +end + +--- Returns the configuration for a buffer +--- +---@param bufnr number +---@return ibl.config.full +M.get_config = function(bufnr) + bufnr = utils.get_bufnr(bufnr) + return merge_configs("merge", M.config or M.default_config, M.buffer_config[bufnr]) +end + +return M diff --git a/lua/ibl/config.types.lua b/lua/ibl/config.types.lua new file mode 100644 index 00000000..963e89f9 --- /dev/null +++ b/lua/ibl/config.types.lua @@ -0,0 +1,224 @@ + +---@meta + +--- Configuration table for indent-blankline +---@class ibl.config +--- Enables or disables indent-blankline +---@field enabled boolean? +--- Sets the amount indent-blankline debounces refreshes in milliseconds +---@field debounce number? +--- Configures the viewport of where indentation guides are generated +---@field viewport_buffer ibl.config.viewport_buffer? +--- Configures the indentation +---@field indent ibl.config.indent? +--- Configures the whitespace +---@field whitespace ibl.config.whitespace? +--- Configures the scope +---@field scope ibl.config.scope? +--- Configures what is excluded from indent-blankline +---@field exclude ibl.config.exclude? + +---@class ibl.config.viewport_buffer +--- Minimum number of lines above and below of what is currently visible in the window for which indentation guides will +--- be generated +---@field min number? +--- Maximum number of lines above and below of what is currently visible in the window for which indentation guides will +--- be generated +---@field max number? + +---@class ibl.config.indent +--- Character, or list of characters, that get used to display the indentation guide +--- +--- Each character has to have a display width of 0 or 1 +---@field char string|string[]? +--- Character, or list of characters, that get used to display the indentation guide for tabs +--- +--- Defaults to what is set in `listchars` +--- Each character has to have a display width of 0 or 1 +---@field tab_char string|string[]? +--- Highlight group, or list of highlight groups, that get applied to the indentation guide +---@field highlight string|string[]? +--- Caps the number of indentation levels by looking at the surrounding code +---@field smart_indent_cap boolean? +--- Virtual text priority for the indentation guide +---@field priority number? + +---@class ibl.config.whitespace +--- Highlight group, or list of highlight groups, that get applied to the whitespace +---@field highlight string|string[]? +--- Removes trailing whitespace on blanklines +--- +--- Turn this off if you want to add background color to the whitespace highlight group +---@field remove_blankline_trail boolean? + +---@class ibl.config.scope +--- Enables or disables scope +---@field enabled boolean? +--- Character, or list of characters, that get used to display the scope indentation guide +--- +--- Each character has to have a display width of 0 or 1 +---@field char string|string[]? +--- Shows an underline on the first line of the scope +---@field show_start boolean? +--- Shows an underline on the last line of the scope +---@field show_end boolean? +--- Checks for the current scope in injected treesitter languages +--- +--- This also influences if the scope gets excluded or not +---@field injected_languages boolean? +--- Highlight group, or list of highlight groups, that get applied to the scope +---@field highlight string|string[]? +--- Virtual text priority for the scope +---@field priority number? +--- Configures additional nodes to be used as scope +---@field include ibl.config.scope.include? +--- Configures nodes or languages to be excluded from scope +---@field exclude ibl.config.scope.exclude? + +---@class ibl.config.scope.include +--- map of language to a list of node types which can be used as scope +--- +--- Use `*` as a wildcard for all languages +--- +--- Example: +--- +--- { +--- ["*"] = { "comment" }, +--- rust = { "identifier" }, +--- } +--- +---@field node_type table? + +---@class ibl.config.scope.exclude +--- List of treesitter languages for which scope is disabled +---@field language string[]? +--- map of language to a list of node types which should not be used as scope +--- +--- Use `*` as a wildcard for all languages +--- +--- Example: +--- +--- { +--- ["*"] = { "comment" }, +--- rust = { "identifier" }, +--- } +--- +---@field node_type table? + +---@class ibl.config.exclude +--- List of `filetypes` for which indent-blankline is disabled +---@field filetypes string[]? +--- List of `buftypes` for which indent-blankline is disabled +---@field buftypes string[]? + +------ + +--- Configuration table for indent-blankline +---@class ibl.config.full: ibl.config +--- Enables or disables indent-blankline +---@field enabled boolean +--- Sets the amount indent-blankline debounces refreshes in milliseconds +---@field debounce number +--- Configures the viewport of where indentation guides are generated +---@field viewport_buffer ibl.config.full.viewport_buffer: ibl.config.viewport_buffer +--- Configures the indentation +---@field indent ibl.config.full.indent: ibl.config.indent +--- Configures the whitespace +---@field whitespace ibl.config.full.whitespace: ibl.config.whitespace +--- Configures the scope +---@field scope ibl.config.full.scope: ig.config.scope +--- Configures what is excluded from indent-blankline +---@field exclude ibl.config.full.exclude: ibl.config.exclude + +---@class ibl.config.full.viewport_buffer: ibl.config.viewport_buffer +--- Minimum number of lines above and below of what is currently visible in the window for which indentation guides will +--- be generated +---@field min number +--- Maximum number of lines above and below of what is currently visible in the window for which indentation guides will +--- be generated +---@field max number + +---@class ibl.config.full.indent: ibl.config.indent +--- Character, or list of characters, that get used to display the indentation guide +--- +--- Each character has to have a display width of 0 or 1 +---@field char string|string[] +--- Character, or list of characters, that get used to display the indentation guide for tabs +--- +--- Defaults to what is set in `listchars` +--- Each character has to have a display width of 0 or 1 +---@field tab_char string|string[]? +--- Highlight group, or list of highlight groups, that get applied to the indentation guide +---@field highlight string|string[] +--- Caps the number of indentation levels by looking at the surrounding code +---@field smart_indent_cap boolean +--- Virtual text priority for the indentation guide +---@field priority number + +---@class ibl.config.full.whitespace: ibl.config.whitespace +--- Highlight group, or list of highlight groups, that get applied to the whitespace +---@field highlight string|string[] +--- Removes trailing whitespace on blanklines +--- +--- Turn this off if you want to add background color to the whitespace highlight group +---@field remove_blankline_trail boolean + +---@class ibl.config.full.scope: ibl.config.scope +--- Enables or disables scope +---@field enabled boolean +--- Character, or list of characters, that get used to display the scope indentation guide +--- +--- Each character has to have a display width of 0 or 1 +---@field char string|string[]? +--- Shows an underline on the first line of the scope +---@field show_start boolean +--- Shows an underline on the last line of the scope +---@field show_end boolean +--- Checks for the current scope in injected treesitter languages +--- +--- This also influences if the scope gets excluded or not +---@field injected_languages boolean +--- Highlight group, or list of highlight groups, that get applied to the scope +---@field highlight string|string[] +--- Virtual text priority for the scope +---@field priority number +--- Configures additional nodes to be used as scope +---@field include ibl.config.full.scope.include +--- Configures nodes or languages to be excluded from scope +---@field exclude ibl.config.full.scope.exclude: ibl.config.scope.exclude + +---@class ibl.config.full.scope.include: ibl.config.scope.include +--- map of language to a list of node types which can be used as scope +--- +--- Use `*` as a wildcard for all languages +--- +--- Example: +--- +--- { +--- ["*"] = { "comment" }, +--- rust = { "identifier" }, +--- } +--- +---@field node_type table + +---@class ibl.config.full.scope.exclude: ibl.config.scope.exclude +--- List of treesitter languages for which scope is disabled +---@field language string[] +--- map of language to a list of node types which should not be used as scope +--- +--- Use `*` as a wildcard for all languages +--- +--- Example: +--- +--- { +--- ["*"] = { "comment" }, +--- rust = { "identifier" }, +--- } +--- +---@field node_type table + +---@class ibl.config.full.exclude: ibl.config.exclude +--- List of `filetypes` for which indent-blankline is disabled +---@field filetypes string[] +--- List of `buftypes` for which indent-blankline is disabled +---@field buftypes string[] diff --git a/lua/ibl/highlights.lua b/lua/ibl/highlights.lua new file mode 100644 index 00000000..cf120720 --- /dev/null +++ b/lua/ibl/highlights.lua @@ -0,0 +1,83 @@ +local conf = require "ibl.config" +local hooks = require "ibl.hooks" + +---@class ibl.highlight +---@field char string +---@field underline string? + +local M = { + ---@type ibl.highlight[] + indent = {}, + ---@type ibl.highlight[] + whitespace = {}, + ---@type ibl.highlight[] + scope = {}, +} + +M.setup = function() + local config = conf.get_config(-1) + + for _, fn in + pairs(hooks.get(-1, hooks.type.HIGHLIGHT_SETUP) --[[ @as ibl.hooks.cb.highlight_setup[] ]]) + do + fn() + end + + local get = function(name) + return vim.api.nvim_get_hl(0, { name = name }) + end + local not_set = function(hl) + return not hl or vim.tbl_count(hl) == 0 + end + + local indent_highlights = config.indent.highlight + if type(indent_highlights) == "string" then + indent_highlights = { indent_highlights } + end + M.indent = {} + for i, name in ipairs(indent_highlights) do + local hl = get(name) + if not_set(hl) then + error(string.format("No highlight group '%s' found", name)) + end + hl.nocombine = true + M.indent[i] = { char = string.format("@ibl.indent.char.%d", i) } + vim.api.nvim_set_hl(0, M.indent[i].char, hl) + end + + local whitespace_highlights = config.whitespace.highlight + if type(whitespace_highlights) == "string" then + whitespace_highlights = { whitespace_highlights } + end + M.whitespace = {} + for i, name in ipairs(whitespace_highlights) do + local hl = get(name) + if not_set(hl) then + error(string.format("No highlight group '%s' found", name)) + end + hl.nocombine = true + M.whitespace[i] = { char = string.format("@ibl.whitespace.char.%d", i) } + vim.api.nvim_set_hl(0, M.whitespace[i].char, hl) + end + + local scope_highlights = config.scope.highlight + if type(scope_highlights) == "string" then + scope_highlights = { scope_highlights } + end + M.scope = {} + for i, scope_name in ipairs(scope_highlights) do + local char_hl = get(scope_name) + if not_set(char_hl) then + error(string.format("No highlight group '%s' found", scope_name)) + end + char_hl.nocombine = true + M.scope[i] = { + char = string.format("@ibl.scope.char.%d", i), + underline = string.format("@ibl.scope.underline.%d", i), + } + vim.api.nvim_set_hl(0, M.scope[i].char, char_hl) + vim.api.nvim_set_hl(0, M.scope[i].underline, { sp = char_hl.fg, underline = true }) + end +end + +return M diff --git a/lua/ibl/hooks.lua b/lua/ibl/hooks.lua new file mode 100644 index 00000000..45b5eeef --- /dev/null +++ b/lua/ibl/hooks.lua @@ -0,0 +1,266 @@ +local conf = require "ibl.config" +local utils = require "ibl.utils" +local indent = require "ibl.indent" +local M = {} + +---@enum ibl.hooks.type +M.type = { + ACTIVE = "ACTIVE", + SCOPE_ACTIVE = "SCOPE_ACTIVE", + SKIP_LINE = "SKIP_LINE", + WHITESPACE = "WHITESPACE", + VIRTUAL_TEXT = "VIRTUAL_TEXT", + SCOPE_HIGHLIGHT = "SCOPE_HIGHLIGHT", + CLEAR = "CLEAR", + HIGHLIGHT_SETUP = "HIGHLIGHT_SETUP", +} + +---@class ibl.hooks.options +---@field bufnr number? +local default_opts = { + bufnr = nil, +} + +local hooks = { + [M.type.ACTIVE] = {}, + [M.type.SCOPE_ACTIVE] = {}, + [M.type.SKIP_LINE] = {}, + [M.type.WHITESPACE] = {}, + [M.type.VIRTUAL_TEXT] = {}, + [M.type.SCOPE_HIGHLIGHT] = {}, + [M.type.CLEAR] = {}, + [M.type.HIGHLIGHT_SETUP] = {}, + buffer_scoped = {}, +} +local count = 0 + +---@alias ibl.hooks.cb.active fun(bufnr: number): boolean +---@alias ibl.hooks.cb.scope_active fun(bufnr: number): boolean +---@alias ibl.hooks.cb.skip_line fun(tick: number, bufnr: number, row: number, line: string): boolean +---@alias ibl.hooks.cb.whitespace fun(tick: number, bufnr: number, row: number, whitespace: ibl.indent.whitespace[]): ibl.indent.whitespace[] +---@alias ibl.hooks.cb.virtual_text fun(tick: number, bufnr: number, row: number, virt_text: ibl.virtual_text): ibl.virtual_text +---@alias ibl.hooks.cb.scope_highlight fun(tick: number, bufnr: number, scope: TSNode, scope_index: number): number +---@alias ibl.hooks.cb.clear fun(bufnr: number) +---@alias ibl.hooks.cb.highlight_setup fun() + +--- Registers a hook +--- +--- Each hook type takes a callback a different function, and a configuration table +---@param type ibl.hooks.type +---@param cb function +---@param opts ibl.hooks.options +---@overload fun(type: 'ACTIVE', cb: ibl.hooks.cb.active, opts: ibl.hooks.options?): string +---@overload fun(type: 'SCOPE_ACTIVE', cb: ibl.hooks.cb.scope_active, opts: ibl.hooks.options?): string +---@overload fun(type: 'SKIP_LINE', cb: ibl.hooks.cb.skip_line, opts: ibl.hooks.options?): string +---@overload fun(type: 'WHITESPACE', cb: ibl.hooks.cb.whitespace, opts: ibl.hooks.options?): string +---@overload fun(type: 'VIRTUAL_TEXT', cb: ibl.hooks.cb.virtual_text, opts: ibl.hooks.options?): string +---@overload fun(type: 'SCOPE_HIGHLIGHT', cb: ibl.hooks.cb.scope_highlight, opts: ibl.hooks.options?): string +---@overload fun(type: 'CLEAR', cb: ibl.hooks.cb.clear, opts: ibl.hooks.options?): string +---@overload fun(type: 'HIGHLIGHT_SETUP', cb: ibl.hooks.cb.highlight_setup, opts: ibl.hooks.options?): string +M.register = function(type, cb, opts) + vim.validate { + type = { + type, + function(t) + return M.type[t] == t + end, + "hooks type enum", + }, + cb = { cb, "function" }, + opts = { opts, "table", true }, + } + opts = vim.tbl_deep_extend("keep", opts or {}, default_opts) + vim.validate { + bufnr = { opts.bufnr, "number", true }, + } + if opts.bufnr then + opts.bufnr = utils.get_bufnr(opts.bufnr) + end + count = count + 1 + local hook_id = type .. "_" .. tostring(count) + + if opts.bufnr then + local bufnr = tostring(opts.bufnr) + if not hooks.buffer_scoped[bufnr] then + hooks.buffer_scoped[bufnr] = { + [M.type.ACTIVE] = {}, + [M.type.SCOPE_ACTIVE] = {}, + [M.type.SKIP_LINE] = {}, + [M.type.WHITESPACE] = {}, + [M.type.VIRTUAL_TEXT] = {}, + [M.type.SCOPE_HIGHLIGHT] = {}, + [M.type.CLEAR] = {}, + [M.type.HIGHLIGHT_SETUP] = {}, + } + end + hooks.buffer_scoped[bufnr][type][hook_id] = cb + else + hooks[type][hook_id] = cb + end + + return hook_id +end + +--- Clears a hook by id +--- +---@param id string +M.clear = function(id) + vim.validate { id = { id, "string" } } + local type, hook_id = unpack(vim.split(id, "_")) + if not type or not hook_id or not vim.tbl_contains(M.type, type) then + return + end + hooks[type][hook_id] = nil +end + +--- Clears all hooks +--- +M.clear_all = function() + hooks = { + [M.type.ACTIVE] = {}, + [M.type.SCOPE_ACTIVE] = {}, + [M.type.SKIP_LINE] = {}, + [M.type.WHITESPACE] = {}, + [M.type.VIRTUAL_TEXT] = {}, + [M.type.SCOPE_HIGHLIGHT] = {}, + [M.type.CLEAR] = {}, + [M.type.HIGHLIGHT_SETUP] = {}, + buffer_scoped = {}, + } +end + +--- Returns all hooks of the given type for a buffer +--- +---@param bufnr number +---@param type ibl.hooks.type +---@overload fun(bufnr: number, type: 'ACTIVE'): ibl.hooks.cb.active[] +---@overload fun(bufnr: number, type: 'SCOPE_ACTIVE'): ibl.hooks.cb.scope_active[] +---@overload fun(bufnr: number, type: 'SKIP_LINE'): ibl.hooks.cb.skip_line[] +---@overload fun(bufnr: number, type: 'WHITESPACE'): ibl.hooks.cb.whitespace[] +---@overload fun(bufnr: number, type: 'VIRTUAL_TEXT'): ibl.hooks.cb.virtual_text[] +---@overload fun(bufnr: number, type: 'SCOPE_HIGHLIGHT'): ibl.hooks.cb.scope_highlight[] +---@overload fun(bufnr: number, type: 'CLEAR'): ibl.hooks.cb.clear[] +---@overload fun(bufnr: number, type: 'HIGHLIGHT_SETUP'): ibl.hooks.cb.highlight_setup[] +M.get = function(bufnr, type) + local bufnr_str = tostring(bufnr) + local list = {} + for _, hook in pairs(hooks[type]) do + table.insert(list, hook) + end + if hooks.buffer_scoped[bufnr_str] then + for _, hook in pairs(hooks.buffer_scoped[bufnr_str][type]) do + table.insert(list, hook) + end + end + + return list +end + +--- Built in hooks +--- +--- You can register them yourself using `hooks.register` +--- +--- +--- hooks.register( +--- hooks.type.SKIP_LINE, +--- hooks.builtin.skip_preproc_lines, +--- { bufnr = 0 } +--- ) +--- +M.builtin = { + ---@type ibl.hooks.cb.skip_line + skip_preproc_lines = function(_, _, _, line) + for _, pattern in ipairs { + "#if", + "#ifdef", + "#ifndef", + "#elif", + "#elifdef", + "#elifndef", + "#else", + "#endif", + } do + if vim.startswith(line, pattern) then + return true + end + end + return false + end, + + ---@type ibl.hooks.cb.scope_highlight + scope_highlight_from_extmark = function(_, bufnr, scope, scope_index) + local config = conf.get_config(bufnr) + local highlight = config.scope.highlight + + if type(highlight) ~= "table" then + return scope_index + end + + local start_row = scope:start() + local end_row = scope:end_() + local start_line = vim.api.nvim_buf_get_lines(bufnr, start_row, start_row + 1, false) + local end_line = vim.api.nvim_buf_get_lines(bufnr, end_row, end_row + 1, false) + local end_pos + local start_pos + + if end_line[1] then + end_pos = vim.inspect_pos(bufnr, end_row, #end_line[1] - 1, { + extmarks = true, + syntax = false, + treesitter = false, + semantic_tokens = false, + }) + end + if start_line[1] then + start_pos = vim.inspect_pos(bufnr, start_row, #start_line[1] - 1, { + extmarks = true, + syntax = false, + treesitter = false, + semantic_tokens = false, + }) + end + + if not end_pos and not start_pos then + return scope_index + end + + for i, hl_group in ipairs(highlight) do + if end_pos then + for _, extmark in ipairs(end_pos.extmarks) do + if extmark.opts.hl_group == hl_group then + return i + end + end + end + if start_pos then + for _, extmark in ipairs(start_pos.extmarks) do + if extmark.opts.hl_group == hl_group then + return i + end + end + end + end + return scope_index + end, + + ---@type ibl.hooks.cb.whitespace + hide_first_space_indent_level = function(_, _, _, whitespace_tbl) + if whitespace_tbl[1] == indent.whitespace.INDENT then + whitespace_tbl[1] = indent.whitespace.SPACE + end + return whitespace_tbl + end, + + ---@type ibl.hooks.cb.whitespace + hide_first_tab_indent_level = function(_, _, _, whitespace_tbl) + if + whitespace_tbl[1] == indent.whitespace.TAB_START + or whitespace_tbl[1] == indent.whitespace.TAB_START_SINGLE + then + whitespace_tbl[1] = indent.whitespace.TAB_FILL + end + return whitespace_tbl + end, +} + +return M diff --git a/lua/ibl/indent.lua b/lua/ibl/indent.lua new file mode 100644 index 00000000..0e394719 --- /dev/null +++ b/lua/ibl/indent.lua @@ -0,0 +1,117 @@ +local M = {} + +---@enum ibl.indent.whitespace +M.whitespace = { + TAB_START = 1, + TAB_START_SINGLE = 2, + TAB_FILL = 3, + TAB_END = 4, + SPACE = 5, + INDENT = 6, +} + +---@class ibl.indent_state +---@field cap boolean +---@field stack table? + +---@class ibl.indent_options +---@field smart_indent_cap boolean +---@field shiftwidth number +---@field tabstop number +---@field vartabstop string + +--- Takes the whitespace of a line and returns a list of ibl.indent.whitespace +--- +---@param whitespace string +---@param opts ibl.indent_options +---@param indent_state ibl.indent_state? +---@return ibl.indent.whitespace[], ibl.indent_state +M.get = function(whitespace, opts, indent_state) + if not indent_state then + indent_state = { cap = false, stack = {} } + end + local shiftwidth = opts.shiftwidth + local tabstop = opts.tabstop + local vartabstop = opts.vartabstop + local spaces = 0 + local tabs = 0 + local extra = 0 + local indent_cap = indent_state.stack[#indent_state.stack] or 0 + if indent_state.cap then + indent_cap = indent_state.stack[1] or 0 + indent_state.cap = false + end + local varts = vim.tbl_map(tonumber, vim.split(vartabstop, ",", { trimempty = true })) + if shiftwidth == 0 then + shiftwidth = tabstop + end + local whitespace_tbl = {} + + for ch in whitespace:gmatch "." do + if ch == "\t" then + local tab_width = tabstop - ((spaces + extra - tabstop) % tabstop) + while #varts > 0 do + tabstop = table.remove(varts, 1) + if tabstop > spaces + extra then + tab_width = tabstop - spaces + extra + break + end + end + tabs = tabs + tab_width + + if tab_width == 1 then + table.insert(whitespace_tbl, M.whitespace.TAB_START_SINGLE) + else + table.insert(whitespace_tbl, M.whitespace.TAB_START) + end + + for i = 2, tab_width do + if i == tab_width then + table.insert(whitespace_tbl, M.whitespace.TAB_END) + else + table.insert(whitespace_tbl, M.whitespace.TAB_FILL) + end + end + else + local mod = (spaces + tabs + extra) % shiftwidth + if vim.tbl_contains(indent_state.stack, spaces + tabs) then + table.insert(whitespace_tbl, M.whitespace.INDENT) + extra = extra + mod + elseif mod == 0 then + if #whitespace_tbl < indent_cap or not opts.smart_indent_cap then + table.insert(whitespace_tbl, M.whitespace.INDENT) + extra = extra + mod + else + indent_state.cap = true + table.insert(whitespace_tbl, M.whitespace.SPACE) + end + else + table.insert(whitespace_tbl, M.whitespace.SPACE) + end + spaces = spaces + 1 + end + end + + indent_state.stack = vim.tbl_filter(function(a) + return a < spaces + tabs + end, indent_state.stack) + table.insert(indent_state.stack, spaces + tabs) + + return whitespace_tbl, indent_state +end + +--- Returns true if the passed whitespace is an indent +--- +---@param whitespace ibl.indent.whitespace +M.is_indent = function(whitespace) + return vim.tbl_contains({ M.whitespace.INDENT, M.whitespace.TAB_START, M.whitespace.TAB_START_SINGLE }, whitespace) +end + +--- Returns true if the passed whitespace belongs to space indent +--- +---@param whitespace ibl.indent.whitespace +M.is_space_indent = function(whitespace) + return vim.tbl_contains({ M.whitespace.INDENT, M.whitespace.SPACE }, whitespace) +end + +return M diff --git a/lua/ibl/init.lua b/lua/ibl/init.lua new file mode 100644 index 00000000..872841d4 --- /dev/null +++ b/lua/ibl/init.lua @@ -0,0 +1,410 @@ +local highlights = require "ibl.highlights" +local hooks = require "ibl.hooks" +local autocmds = require "ibl.autocmds" +local inlay_hints = require "ibl.inlay_hints" +local indent = require "ibl.indent" +local vt = require "ibl.virt_text" +local scp = require "ibl.scope" +local conf = require "ibl.config" +local utils = require "ibl.utils" + +local namespace = vim.api.nvim_create_namespace "indent_blankline" + +local M = {} + +---@package +M.initialized = false + +---@type table +local global_buffer_state = {} + +---@param bufnr number +local clear_buffer = function(bufnr) + vt.clear_buffer(bufnr) + inlay_hints.clear_buffer(bufnr) + for _, fn in pairs(hooks.get(bufnr, hooks.type.CLEAR)) do + fn(bufnr) + end +end + +---@param config ibl.config.full +local setup = function(config) + M.initialized = true + + if not config.enabled then + for bufnr, _ in pairs(global_buffer_state) do + clear_buffer(bufnr) + end + global_buffer_state = {} + inlay_hints.clear() + return + end + + vim.schedule_wrap(function() + inlay_hints.setup() + highlights.setup() + autocmds.setup() + + M.refresh_all() + end)() +end + +--- Initializes and configures indent-blankline. +--- +--- Optionally, the first parameter can be a configuration table. +--- All values that are not passed in the table are set to the default value. +--- List values get merged with the default list value. +--- +--- `setup` is idempotent, meaning you can call it multiple times, and each call will reset indent-blankline. +--- If you want to only update the current configuration, use `update()`. +---@param config ibl.config? +M.setup = function(config) + setup(conf.set_config(config)) +end + +--- Updates the indent-blankline configuration +--- +--- The first parameter is a configuration table. +--- All values that are not passed in the table are kept as they are. +--- List values get merged with the current list value. +---@param config ibl.config +M.update = function(config) + setup(conf.update_config(config)) +end + +--- Overwrites the indent-blankline configuration +--- +--- The first parameter is a configuration table. +--- All values that are not passed in the table are kept as they are. +--- All values that are passed overwrite existing and default values. +---@param config ibl.config +M.overwrite = function(config) + setup(conf.overwrite_config(config)) +end + +--- Configures indent-blankline for one buffer +--- +--- All values that are not passed are cleared, and will fall back to the global config +---@param bufnr number +---@param config ibl.config +M.setup_buffer = function(bufnr, config) + assert(M.initialized, "Tried to setup buffer without doing global setup") + bufnr = utils.get_bufnr(bufnr) + local c = conf.set_buffer_config(bufnr, config) + + if c.enabled then + M.refresh(bufnr) + else + clear_buffer(bufnr) + end +end + +--- Refreshes indent-blankline in all buffers +M.refresh_all = function() + for _, win in ipairs(vim.api.nvim_list_wins()) do + vim.api.nvim_win_call(win, function() + M.refresh(vim.api.nvim_win_get_buf(win) --[[@as number]]) + end) + end +end + +local debounced_refresh = setmetatable({ + timers = {}, + queued_buffers = {}, +}, { + ---@param bufnr number + __call = function(self, bufnr) + bufnr = utils.get_bufnr(bufnr) + local uv = vim.uv or vim.loop + if not self.timers[bufnr] then + self.timers[bufnr] = uv.new_timer() + end + if uv.timer_get_due_in(self.timers[bufnr]) <= 50 then + M.refresh(bufnr) + + local config = conf.get_config(bufnr) + self.timers[bufnr]:start(config.debounce, 0, function() + if self.queued_buffers[bufnr] then + self.queued_buffers[bufnr] = nil + vim.schedule_wrap(M.refresh)(bufnr) + end + end) + else + self.queued_buffers[bufnr] = true + end + end, +}) + +--- Refreshes indent-blankline in one buffer, debounced +--- +---@param bufnr number +M.debounced_refresh = function(bufnr) + if vim.api.nvim_get_current_buf() == bufnr and vim.api.nvim_get_option_value("scrollbind", { scope = "local" }) then + for _, b in ipairs(vim.fn.tabpagebuflist()) do + debounced_refresh(b) + end + else + debounced_refresh(bufnr) + end +end + +--- Refreshes indent-blankline in one buffer +--- +--- Only use this directly if you know what you are doing, consider `debounced_refresh` instead +---@param bufnr number +M.refresh = function(bufnr) + assert(M.initialized, "Tried to refresh without doing setup") + bufnr = utils.get_bufnr(bufnr) + local is_current_buffer = vim.api.nvim_get_current_buf() == bufnr + local config = conf.get_config(bufnr) + + if not config.enabled or not vim.api.nvim_buf_is_loaded(bufnr) or not utils.is_buffer_active(bufnr, config) then + clear_buffer(bufnr) + return + end + + for _, fn in + pairs(hooks.get(bufnr, hooks.type.ACTIVE) --[[ @as ibl.hooks.cb.active[] ]]) + do + if not fn(bufnr) then + clear_buffer(bufnr) + return + end + end + + local left_offset, top_offset, win_end, win_height = utils.get_offset(bufnr) + if top_offset >= win_end then + return + end + + local offset = math.max(top_offset - 1 - config.viewport_buffer.min, 0) + + local scope_disabled = false + for _, fn in + pairs(hooks.get(bufnr, hooks.type.SCOPE_ACTIVE) --[[ @as ibl.hooks.cb.scope_active[] ]]) + do + if not fn(bufnr) then + scope_disabled = true + break + end + end + + local scope + if not scope_disabled and config.scope.enabled then + scope = scp.get(bufnr, config) + if scope and scope:start() >= 0 then + offset = top_offset - math.min(top_offset - math.min(offset, scope:start()), config.viewport_buffer.max) + end + end + + local range = math.min(win_end + config.viewport_buffer.min, vim.api.nvim_buf_line_count(bufnr)) + local lines = vim.api.nvim_buf_get_lines(bufnr, offset, range, false) + + ---@type ibl.indent_options + local indent_opts = { + tabstop = vim.api.nvim_get_option_value("tabstop", { buf = bufnr }), + vartabstop = vim.api.nvim_get_option_value("vartabstop", { buf = bufnr }), + shiftwidth = vim.api.nvim_get_option_value("shiftwidth", { buf = bufnr }), + smart_indent_cap = config.indent.smart_indent_cap, + } + local listchars = utils.get_listchars(bufnr) + if listchars.tabstop_overwrite then + indent_opts.tabstop = 2 + indent_opts.vartabstop = "" + end + + local indent_state + local next_virtual_string = {} + local empty_line_counter = 0 + + local buffer_state = global_buffer_state[bufnr] + or { + scope = nil, + left_offset = -1, + top_offset = -1, + tick = 0, + } + + local same_scope = (scope and scope:id()) == (buffer_state.scope and buffer_state.scope:id()) + + if not same_scope then + inlay_hints.clear_buffer(bufnr) + end + + global_buffer_state[bufnr] = { + left_offset = left_offset, + top_offset = top_offset, + scope = scope, + tick = buffer_state.tick + 1, + } + + local scope_col_start_single = -1 + local scope_row_start, scope_col_start, scope_row_end, scope_col_end = -1, -1, -1, -1 + local scope_index = -1 + if scope then + scope_row_start, scope_col_start, scope_row_end, scope_col_end = scope:range() + scope_row_start, scope_col_start, scope_row_end = scope_row_start + 1, scope_col_start + 1, scope_row_end + 1 + end + + local last_whitespace_tbl = {} + + for i, line in ipairs(lines) do + local row = i + offset + local whitespace = utils.get_whitespace(line) + local foldclosed = vim.fn.foldclosed(row) + + for _, fn in + pairs(hooks.get(bufnr, hooks.type.SKIP_LINE) --[[ @as ibl.hooks.cb.skip_line[] ]]) + do + if fn(buffer_state.tick, bufnr, row - 1, line) then + vt.clear_buffer(bufnr, row) + goto continue + end + end + + if is_current_buffer and foldclosed == row then + local foldtext = vim.fn.foldtextresult(row) + local foldtext_whitespace = utils.get_whitespace(foldtext) + if vim.fn.strdisplaywidth(foldtext_whitespace, 0) < vim.fn.strdisplaywidth(whitespace, 0) then + vt.clear_buffer(bufnr, row) + goto continue + end + end + + if is_current_buffer and foldclosed > -1 and foldclosed + win_height < row then + vt.clear_buffer(bufnr, row) + goto continue + end + + local blankline = line:len() == 0 + local whitespace_only = not blankline and line == whitespace + local whitespace_tbl + local scope_active = row >= scope_row_start and row <= scope_row_end + local scope_start = row == scope_row_start + local scope_end = row == scope_row_end + + -- #### calculate indent #### + if not blankline then + whitespace_tbl, indent_state = indent.get(whitespace, indent_opts, indent_state) + elseif empty_line_counter > 0 then + empty_line_counter = empty_line_counter - 1 + whitespace_tbl = next_virtual_string + else + if i == #lines then + whitespace_tbl = {} + else + local j = i + 1 + while j < #lines and lines[j]:len() == 0 do + j = j + 1 + empty_line_counter = empty_line_counter + 1 + end + local j_whitespace = utils.get_whitespace(lines[j]) + whitespace_tbl, indent_state = indent.get(j_whitespace, indent_opts, indent_state) + + if utils.has_end(lines[j]) then + local trail = last_whitespace_tbl[indent_state.stack[#indent_state.stack] + 1] + local trail_whitespace = last_whitespace_tbl[indent_state.stack[#indent_state.stack]] + if trail then + table.insert(whitespace_tbl, trail) + elseif trail_whitespace then + if indent.is_space_indent(trail_whitespace) then + table.insert(whitespace_tbl, indent.whitespace.INDENT) + else + table.insert(whitespace_tbl, indent.whitespace.TAB_START) + end + end + end + end + next_virtual_string = whitespace_tbl + end + + -- remove blankline trail + if blankline and config.whitespace.remove_blankline_trail then + while #whitespace_tbl > 0 do + if indent.is_indent(whitespace_tbl[#whitespace_tbl]) then + break + end + table.remove(whitespace_tbl, #whitespace_tbl) + end + end + + -- Fix horizontal scroll + local current_left_offset = left_offset + while #whitespace_tbl > 0 and current_left_offset > 0 do + table.remove(whitespace_tbl, 1) + current_left_offset = current_left_offset - 1 + end + + for _, fn in + pairs(hooks.get(bufnr, hooks.type.WHITESPACE) --[[ @as ibl.hooks.cb.whitespace[] ]]) + do + whitespace_tbl = fn(buffer_state.tick, bufnr, row - 1, whitespace_tbl) + end + + last_whitespace_tbl = whitespace_tbl + + -- #### make virtual text #### + if scope_start and scope then + scope_col_start = #whitespace + scope_col_start_single = #whitespace_tbl + scope_index = #vim.tbl_filter(function(w) + return indent.is_indent(w) + end, whitespace_tbl) + 1 + for _, fn in + pairs(hooks.get(bufnr, hooks.type.SCOPE_HIGHLIGHT) --[[ @as ibl.hooks.cb.scope_highlight[] ]]) + do + scope_index = fn(buffer_state.tick, bufnr, scope, scope_index) + end + end + + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + local virt_text, scope_hl = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + -- #### set virtual text #### + vt.clear_buffer(bufnr, row) + + -- Scope start + if config.scope.show_start and scope_start then + vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, #whitespace, { + end_col = #line, + hl_group = scope_hl.underline, + priority = config.scope.priority, + strict = false, + }) + inlay_hints.set(bufnr, row - 1, #whitespace, scope_hl.underline, scope_hl.underline) + end + + -- Scope end + if config.scope.show_end and scope_end and #whitespace_tbl > scope_col_start_single then + vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, scope_col_start, { + end_col = scope_col_end, + hl_group = scope_hl.underline, + priority = config.scope.priority, + strict = false, + }) + inlay_hints.set(bufnr, row - 1, #whitespace, scope_hl.underline, scope_hl.underline) + end + + for _, fn in + pairs(hooks.get(bufnr, hooks.type.VIRTUAL_TEXT) --[[ @as ibl.hooks.cb.virtual_text[] ]]) + do + virt_text = fn(buffer_state.tick, bufnr, row - 1, virt_text) + end + + -- Indent + if #virt_text > 0 then + vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, 0, { + virt_text = virt_text, + virt_text_pos = "overlay", + hl_mode = "combine", + priority = config.indent.priority, + strict = false, + }) + end + + ::continue:: + end +end + +return M diff --git a/lua/ibl/inlay_hints.lua b/lua/ibl/inlay_hints.lua new file mode 100644 index 00000000..5fd5ee0b --- /dev/null +++ b/lua/ibl/inlay_hints.lua @@ -0,0 +1,86 @@ +local inlayhint_namespace = vim.api.nvim_create_namespace "vim_lsp_inlayhint" + +local M = {} + +---@type function? +local handler = nil + +---@type table +local buffer_state = {} + +---@param bufnr number +---@param row number +---@param col number +---@param hl string|string[] +---@param hl_empty string +local set_extmark = function(bufnr, row, col, hl, hl_empty) + local inlayhint_extmarks = vim.api.nvim_buf_get_extmarks( + bufnr, + inlayhint_namespace, + { row, col }, + { row, -1 }, + { details = true, hl_name = false, type = "virt_text" } + ) + + vim.api.nvim_buf_clear_namespace(bufnr, inlayhint_namespace, row, row + 1) + for _, inlay in ipairs(inlayhint_extmarks or {}) do + local _, inlay_row, inlay_col, inlay_opt = unpack(inlay) + for _, virt_text in ipairs(inlay_opt.virt_text) do + if vim.trim(virt_text[1]) == "" then + virt_text[2] = hl_empty + else + virt_text[2] = hl + end + end + inlay_opt.ns_id = nil + pcall(vim.api.nvim_buf_set_extmark, bufnr, inlayhint_namespace, inlay_row, inlay_col, inlay_opt) + end +end + +M.setup = function() + if not handler then + handler = vim.lsp.handlers["textDocument/inlayHint"] + + vim.lsp.handlers["textDocument/inlayHint"] = function(err, result, ctx, conf) + if handler then + handler(err, result, ctx, conf) + end + require("ibl").debounced_refresh(ctx.bufnr) + end + end +end + +M.clear = function() + if handler then + vim.lsp.handlers["textDocument/inlayHint"] = handler + handler = nil + end + for bufnr, _ in pairs(buffer_state) do + M.clear_buffer(bufnr) + end +end + +---@param bufnr number +M.clear_buffer = function(bufnr) + for _, row in ipairs(buffer_state[bufnr] or {}) do + set_extmark(bufnr, row, 0, "LspInlayHint", "") + end + + buffer_state[bufnr] = nil +end + +---@param bufnr number +---@param row number +---@param col number +---@param hl string +---@param hl_empty string +M.set = function(bufnr, row, col, hl, hl_empty) + if not buffer_state[bufnr] then + buffer_state[bufnr] = {} + end + table.insert(buffer_state[bufnr], row) + + set_extmark(bufnr, row, col, { "LspInlayHint", hl }, hl_empty) +end + +return M diff --git a/lua/ibl/scope.lua b/lua/ibl/scope.lua new file mode 100644 index 00000000..3f3e7d79 --- /dev/null +++ b/lua/ibl/scope.lua @@ -0,0 +1,90 @@ +local utils = require "ibl.utils" +local scope_lang = require "ibl.scope_languages" +local M = {} + +---@param win number +---@return table +M.get_cursor_range = function(win) + local pos = vim.api.nvim_win_get_cursor(win) + local row, col = pos[1] - 1, pos[2] + return { row, 0, row, col } +end + +--- Takes a language tree and a range, and returns the child language tree for that range +--- +---@param language_tree LanguageTree +---@param range table +---@param config ibl.config.full +M.language_for_range = function(language_tree, range, config) + if config.scope.injected_languages then + for _, child in pairs(language_tree:children()) do + if child:contains(range) then + local lang_tree = M.language_for_range(child, range, config) + if lang_tree then + return lang_tree + end + end + end + end + + if not vim.tbl_contains(config.scope.exclude.language, language_tree:lang()) then + return language_tree + end +end + +---@param bufnr number +---@param config ibl.config.full +---@return TSNode? +M.get = function(bufnr, config) + local lang_tree_ok, lang_tree = pcall(vim.treesitter.get_parser, bufnr) + if not lang_tree_ok or not lang_tree then + return nil + end + + local win + if bufnr ~= vim.api.nvim_get_current_buf() then + local win_list = vim.fn.win_findbuf(bufnr) + win = win_list and win_list[1] + if not win then + return nil + end + else + win = 0 + end + + local range = M.get_cursor_range(win) + lang_tree = M.language_for_range(lang_tree, range, config) + if not lang_tree then + return nil + end + + local lang = lang_tree:lang() + if not scope_lang[lang] then + return nil + end + + local node = lang_tree:named_node_for_range(range, { bufnr = bufnr }) + if not node then + return nil + end + + local excluded_node_types = + utils.tbl_join(config.scope.exclude.node_type["*"] or {}, config.scope.exclude.node_type[lang] or {}) + local include_node_types = + utils.tbl_join(config.scope.include.node_type["*"] or {}, config.scope.include.node_type[lang] or {}) + + while node do + local type = node:type() + + if + (scope_lang[lang][type] and not vim.tbl_contains(excluded_node_types, type)) + or vim.tbl_contains(include_node_types, type) + then + return node + else + node = node:parent() + end + end +end + +return M diff --git a/lua/ibl/scope_languages.lua b/lua/ibl/scope_languages.lua new file mode 100644 index 00000000..458742db --- /dev/null +++ b/lua/ibl/scope_languages.lua @@ -0,0 +1,687 @@ +-- from nvim-treesitter/queries/{lang}/locals + +local M = { + ada = { + compilation = true, + package_declaration = true, + package_body = true, + subprogram_declaration = true, + subprogram_body = true, + block_statement = true, + }, + bash = { + function_definition = true, + }, + bass = { + list = true, + scope = true, + cons = true, + }, + bicep = { + infrastructure = true, + call_expression = true, + + lambda_expression = true, + subscript_expression = true, + + if_statement = true, + for_statement = true, + + array = true, + object = true, + interpolation = true, + }, + bitbake = { + python_function_definition = true, + dictionary_comprehension = true, + list_comprehension = true, + set_comprehension = true, + }, + c = { + preproc_function_def = true, + for_statement = true, + if_statement = true, + while_statement = true, + translation_unit = true, + function_definition = true, + compound_statement = true, + struct_specifier = true, + }, + c_sharp = { + block = true, + }, + cairo = { + program = true, + block = true, + function_definition = true, + loop_expression = true, + if_expression = true, + match_expression = true, + match_arm = true, + + struct_item = true, + enum_item = true, + impl_item = true, + }, + capnp = { + message = true, + annotation_targets = true, + const_list = true, + enum = true, + interface = true, + implicit_generics = true, + generics = true, + group = true, + method_parameters = true, + named_return_types = true, + struct = true, + struct_shorthand = true, + union = true, + }, + commonlisp = { + defun = true, + sym_lit = true, + loop_macro = true, + list_lit = true, + }, + corn = { + object = true, + array = true, + }, + cpon = { + document = true, + + meta_map = true, + map = true, + array = true, + }, + cue = { + source_file = true, + field = true, + for_clause = true, + }, + dart = { + body = true, + block = true, + if_statement = true, + for_statement = true, + while_statement = true, + try_statement = true, + catch_clause = true, + finally_clause = true, + }, + devicetree = { + node = true, + integer_cells = true, + }, + ecma = { + statement_block = true, + ["function"] = true, + arrow_function = true, + function_declaration = true, + method_definition = true, + for_statement = true, + for_in_statement = true, + catch_clause = true, + }, + elixir = { + call = true, + stab_clause = true, + }, + elsa = { + source_file = true, + reduction = true, + }, + fennel = { + program = true, + fn = true, + lambda = true, + let = true, + each = true, + ["for"] = true, + match = true, + }, + firrtl = { + source_file = true, + circuit = true, + module = true, + + ["else"] = true, + when = true, + }, + fish = { + command = true, + function_definition = true, + if_statement = true, + for_statement = true, + begin_statement = true, + while_statement = true, + switch_statement = true, + }, + forth = { + word_definition = true, + }, + fusion = { + block = true, + eel_arrow_function = true, + eel_object = true, + }, + gdscript = { + if_statement = true, + elif_clause = true, + else_clause = true, + for_statement = true, + while_statement = true, + function_definition = true, + constructor_definition = true, + class_definition = true, + match_statement = true, + pattern_section = true, + lambda = true, + get_body = true, + set_body = true, + }, + gleam = { + function_body = true, + case_clause = true, + }, + glimmer = { + element_node = true, + block_statement = true, + }, + go = { + func_literal = true, + source_file = true, + function_declaration = true, + if_statement = true, + block = true, + expression_switch_statement = true, + for_statement = true, + method_declaration = true, + }, + godot_resource = { + section = true, + }, + hare = { + module = true, + function_declaration = true, + if_statement = true, + for_statement = true, + match_expression = true, + switch_expression = true, + }, + heex = { + component = true, + slot = true, + tag = true, + }, + html = { + element = true, + }, + java = { + program = true, + body = true, + lambda_expression = true, + enhanced_for_statement = true, + block = true, + if_statement = true, + consequence = true, + alternative = true, + try_statement = true, + catch_clause = true, + for_statement = true, + constructor_declaration = true, + method_declaration = true, + }, + json = { + object = true, + array = true, + }, + jsonnet = { + parenthesis = true, + anonymous_function = true, + object = true, + field = true, + local_bind = true, + }, + julia = { + function_definition = true, + short_function_definition = true, + macro_definition = true, + for_statement = true, + while_statement = true, + try_statement = true, + catch_clause = true, + finally_clause = true, + let_statement = true, + quote_statement = true, + do_clause = true, + }, + kconfig = { + config = true, + menuconfig = true, + choice = true, + comment_entry = true, + menu = true, + ["if"] = true, + }, + kdl = { + document = true, + node = true, + node_children = true, + }, + kotlin = { + if_expression = true, + when_expression = true, + when_entry = true, + + for_statement = true, + while_statement = true, + do_while_statement = true, + + lambda_literal = true, + function_declaration = true, + primary_constructor = true, + secondary_constructor = true, + anonymous_initializer = true, + + class_declaration = true, + enum_class_body = true, + enum_entry = true, + + interpolated_expression = true, + }, + lua = { + chunk = true, + do_statement = true, + while_statement = true, + repeat_statement = true, + if_statement = true, + for_statement = true, + function_declaration = true, + function_definition = true, + }, + matlab = { + function_definition = true, + }, + mlir = { + region = true, + }, + nix = { + let_expression = true, + rec_attrset_expression = true, + function_expression = true, + }, + ocaml = { + compilation_unit = true, + structure = true, + signature = true, + module_binding = true, + functor = true, + let_binding = true, + match_case = true, + class_binding = true, + class_function = true, + method_definition = true, + let_expression = true, + fun_expression = true, + for_expression = true, + let_class_expression = true, + object_expression = true, + attribute_payload = true, + }, + odin = { + block = true, + declaration = true, + statement = true, + }, + pascal = { + root = true, + + defProc = true, + lambda = true, + declProc = true, + declProcRef = true, + + exceptionHandler = true, + }, + php = { + class_declaration = true, + method_declaration = true, + function_definition = true, + anonymous_function_creation_expression = true, + }, + pony = { + use_statement = true, + actor_definition = true, + class_definition = true, + primitive_definition = true, + interface_definition = true, + trait_definition = true, + struct_definition = true, + + constructor = true, + method = true, + behavior = true, + + if_statement = true, + iftype_statement = true, + elseif_block = true, + elseiftype_block = true, + else_block = true, + for_statement = true, + while_statement = true, + try_statement = true, + with_statement = true, + repeat_statement = true, + recover_statement = true, + match_statement = true, + case_statement = true, + parenthesized_expression = true, + tuple_expression = true, + + array_literal = true, + object_literal = true, + }, + puppet = { + block = true, + defined_resource_type = true, + parameter_list = true, + attribute_type_entry = true, + class_definition = true, + node_definition = true, + resource_declaration = true, + selector = true, + method_call = true, + case_statement = true, + hash = true, + array = true, + }, + python = { + module = true, + class_definition = true, + function_definition = true, + dictionary_comprehension = true, + list_comprehension = true, + set_comprehension = true, + }, + ql = { + module = true, + dataclass = true, + datatype = true, + select = true, + body = true, + conjunction = true, + }, + query = { + program = true, + named_node = true, + anonymous_node = true, + grouping = true, + }, + r = { + function_definition = true, + }, + rasi = { + rule_set = true, + }, + re2c = { + body = true, + }, + ron = { + source_file = true, + array = true, + map = true, + struct = true, + tuple = true, + }, + rst = { + document = true, + directive = true, + }, + ruby = { + method = true, + class = true, + block = true, + do_block = true, + }, + rust = { + block = true, + function_item = true, + closure_expression = true, + while_expression = true, + for_expression = true, + loop_expression = true, + if_expression = true, + match_expression = true, + match_arm = true, + expression_statement = true, + + struct_item = true, + enum_item = true, + impl_item = true, + }, + scala = { + template_body = true, + lambda_expression = true, + function_definition = true, + block = true, + }, + smali = { + class_directive = true, + expression = true, + annotation_directive = true, + array_data_directive = true, + method_definition = true, + packed_switch_directive = true, + sparse_switch_directive = true, + subannotation_directive = true, + }, + sparql = { + triples_block = true, + }, + squirrel = { + script = true, + class_declaration = true, + enum_declaration = true, + function_declaration = true, + attribute_declaration = true, + + array = true, + block = true, + table = true, + anonymous_function = true, + parenthesized_expression = true, + + if_statement = true, + else_statement = true, + while_statement = true, + do_while_statement = true, + switch_statement = true, + for_statement = true, + foreach_statement = true, + try_statement = true, + catch_statement = true, + }, + starlark = { + module = true, + function_definition = true, + dictionary_comprehension = true, + list_comprehension = true, + set_comprehension = true, + }, + supercollider = { + function_call = true, + code_block = true, + function_block = true, + control_structure = true, + }, + swift = { + statements = true, + for_statement = true, + while_statement = true, + repeat_while_statement = true, + do_statement = true, + if_statement = true, + guard_statement = true, + switch_statement = true, + property_declaration = true, + function_declaration = true, + class_declaration = true, + protocol_declaration = true, + }, + systemtap = { + function_definition = true, + statement_block = true, + if_statement = true, + while_statement = true, + for_statement = true, + foreach_statement = true, + catch_clause = true, + }, + t32 = { + block = true, + }, + tablegen = { + class = true, + multiclass = true, + def = true, + defm = true, + defset = true, + defvar = true, + foreach = true, + ["if"] = true, + let = true, + }, + teal = { + anon_function = true, + function_statement = true, + program = true, + if_statement = true, + for_body = true, + repeat_statement = true, + while_body = true, + do_statement = true, + }, + thrift = { + document = true, + definition = true, + }, + tiger = { + for_expression = true, + let_expression = true, + function_declaration = true, + }, + tlaplus = { + bounded_quantification = true, + choose = true, + function_definition = true, + function_literal = true, + lambda = true, + let_in = true, + module = true, + module_definition = true, + operator_definition = true, + set_filter = true, + set_map = true, + unbounded_quantification = true, + non_terminal_proof = true, + suffices_proof_step = true, + theorem = true, + pcal_algorithm = true, + pcal_macro = true, + pcal_procedure = true, + pcal_with = true, + }, + toml = { + table = true, + table_array_element = true, + }, + turlte = { + turtle_doc = true, + }, + ungrammar = { + grammar = true, + }, + usd = { + block = true, + metadata = true, + }, + uxntal = { + program = true, + macro = true, + memory_execution = true, + subroutine = true, + }, + v = { + source_file = true, + function_declaration = true, + if_expression = true, + block = true, + for_statement = true, + }, + verilog = { + loop_generate_construct = true, + loop_statement = true, + conditional_statement = true, + case_item = true, + function_declaration = true, + always_construct = true, + module_declaration = true, + }, + vim = { + script_file = true, + function_definition = true, + }, + wing = { + block = true, + }, + yaml = { + stream = true, + document = true, + block_node = true, + }, + yuck = { + ast_block = true, + list = true, + array = true, + expr = true, + json_array = true, + json_object = true, + parenthesized_expression = true, + }, +} + +M.cpp = vim.tbl_extend("keep", M.c, { + class_specifier = true, + template_declaration = true, + body = true, + template_function = true, + template_method = true, + function_declarator = true, + lambda_expression = true, + catch_clause = true, + requires_expression = true, +}) +M.arduion = M.cpp +M.cuda = M.cpp +M.astro = M.html +M.glsl = M.c +M.hjson = M.json +M.hlsl = M.cpp +M.ispc = vim.tbl_extend("keep", M.c, { + template_declaration = true, + foreach_statement = true, + foreach_instance_statement = true, + unmasked_statement = true, +}) +M.javascript = vim.tbl_extend("keep", M.ecma, { jsx_element = true }) +M.jsonc = M.json +M.luau = M.lua +M.nqc = M.c +M.objc = M.c +M.ocaml_interface = M.ocaml +M.tsx = vim.tbl_extend("keep", M.ecma, { jsx_element = true }) +M.typescript = M.ecma + +return M diff --git a/lua/ibl/utils.lua b/lua/ibl/utils.lua new file mode 100644 index 00000000..bd9e89a3 --- /dev/null +++ b/lua/ibl/utils.lua @@ -0,0 +1,182 @@ +local M = {} + +---@param line string? +M.get_whitespace = function(line) + if not line then + return "" + end + return string.match(line, "^%s+") or "" +end + +---@class ibl.listchars +---@field tabstop_overwrite boolean +---@field space_char string +---@field trail_char string? +---@field lead_char string? +---@field multispace_chars string[]? +---@field leadmultispace_chars string[]? +---@field tab_char_start string? +---@field tab_char_fill string +---@field tab_char_end string? + +---@param bufnr number +---@return ibl.listchars +M.get_listchars = function(bufnr) + local listchars + local list = vim.opt.list:get() + if list then + listchars = vim.opt.listchars:get() + end + + if bufnr ~= vim.api.nvim_get_current_buf() then + local win_list = vim.fn.win_findbuf(bufnr) + local win = win_list and win_list[1] + if win then + list = vim.api.nvim_get_option_value("list", { win = win }) + if list then + local raw_value = vim.api.nvim_get_option_value("listchars", { win = win }) + listchars = {} + for _, key_value_str in ipairs(vim.split(raw_value, ",")) do + local key, value = unpack(vim.split(key_value_str, ":")) + listchars[vim.trim(key)] = value + end + end + end + end + + if list then + local tabstop_overwrite = false + local tab_char + local space_char = listchars.space or " " + local multispace_chars + local leadmultispace_chars + if listchars.tab then + tab_char = vim.fn.split(listchars.tab, "\\zs") + else + tabstop_overwrite = true + tab_char = { "^", "I" } + end + if listchars.multispace then + multispace_chars = vim.fn.split(listchars.multispace, "\\zs") + end + if listchars.leadmultispace then + leadmultispace_chars = vim.fn.split(listchars.leadmultispace, "\\zs") + end + return { + tabstop_overwrite = tabstop_overwrite, + space_char = space_char, + trail_char = listchars.trail, + multispace_char = multispace_chars, + leadmultispace_char = leadmultispace_chars, + lead_char = listchars.lead, + tab_char_start = tab_char[1] or space_char, + tab_char_fill = tab_char[2] or space_char, + tab_char_end = tab_char[3], + } + end + return { + tabstop_overwrite = false, + space_char = " ", + trail_char = nil, + lead_char = nil, + multispace_chars = nil, + leadmultispace_chars = nil, + tab_char_start = nil, + tab_char_fill = " ", + tab_char_end = nil, + } +end + +---@param bufnr number +M.get_filetypes = function(bufnr) + return vim.split( + vim.api.nvim_get_option_value("filetype", { buf = bufnr }), + ".", + { plain = true, trimempty = true } + ) +end + +local has_end_reg = vim.regex "^\\s*\\(}\\|]\\|)\\|end\\)" +---@param line string +M.has_end = function(line) + if has_end_reg and has_end_reg:match_str(line) ~= nil then + return true + end + return false +end + +---@param bufnr number +M.get_offset = function(bufnr) + local win = 0 + local win_view + local win_end + if bufnr == vim.api.nvim_get_current_buf() then + win_view = vim.fn.winsaveview() + win_end = vim.fn.line "w$" + else + local win_list = vim.fn.win_findbuf(bufnr) + if not win_list or not win_list[1] then + return 0, 0, 0, 0 + end + win = win_list[1] + win_view = vim.api.nvim_win_call(win, vim.fn.winsaveview) + end + + local win_height = vim.api.nvim_win_get_height(win) + if not win_end then + win_end = win_height + (win_view.topline or 0) + end + if win_view.lnum > win_end then + win_view.topline = win_view.lnum + win_end = win_view.lnum + win_height + end + + return win_view.leftcol or 0, win_view.topline or 0, win_end, win_height +end + +---@param bufnr number +---@param config ibl.config +M.is_buffer_active = function(bufnr, config) + for _, filetype in ipairs(M.get_filetypes(bufnr)) do + if vim.tbl_contains(config.exclude.filetypes, filetype) then + return false + end + end + + local buftype = vim.api.nvim_get_option_value("buftype", { buf = bufnr }) + if vim.tbl_contains(config.exclude.buftypes, buftype) then + return false + end + + return true +end + +---@param bufnr number +---@return number +M.get_bufnr = function(bufnr) + if not bufnr or bufnr == 0 then + return vim.api.nvim_get_current_buf() --[[@as number]] + end + return bufnr +end + +---@generic T: table +---@vararg T +---@return T +M.tbl_join = function(...) + local result = {} + for i, v in ipairs(vim.tbl_flatten { ... }) do + result[i] = v + end + return result +end + +---@generic T +---@param list T[] +---@param i number +---@return T +M.tbl_get_index = function(list, i) + return list[((i - 1) % #list) + 1] +end + +return M diff --git a/lua/ibl/virt_text.lua b/lua/ibl/virt_text.lua new file mode 100644 index 00000000..0886e757 --- /dev/null +++ b/lua/ibl/virt_text.lua @@ -0,0 +1,124 @@ +local highlights = require "ibl.highlights" +local utils = require "ibl.utils" +local indent = require "ibl.indent" +local whitespace = indent.whitespace +local M = {} + +---@alias ibl.virtual_text { [1]: string, [2]: string|string[] }[] +---@alias ibl.char_map { [ibl.indent.whitespace]: string|string[] } + +---@param input string|string[] +---@param index number +---@return string +local get_char = function(input, index) + if type(input) == "string" then + return input + end + return utils.tbl_get_index(input, index) +end + +---@param config ibl.config +---@param listchars ibl.listchars +---@param whitespace_only boolean +---@param blankline boolean +---@return ibl.char_map +M.get_char_map = function(config, listchars, whitespace_only, blankline) + return { + [whitespace.TAB_START] = config.indent.tab_char or listchars.tab_char_start or config.indent.char, + [whitespace.TAB_START_SINGLE] = config.indent.tab_char + or listchars.tab_char_end + or listchars.tab_char_start + or config.indent.char, + [whitespace.TAB_FILL] = (blankline and " ") or listchars.tab_char_fill, + [whitespace.TAB_END] = (blankline and " ") or listchars.tab_char_end or listchars.tab_char_fill, + [whitespace.SPACE] = (blankline and " ") + or (whitespace_only and (listchars.trail_char or listchars.multispace_chars or listchars.space_char)) + or listchars.leadmultispace_chars + or listchars.lead_char + or listchars.multispace_chars + or listchars.space_char, + [whitespace.INDENT] = config.indent.char, + } +end + +---@param bufnr number +---@param row number? +M.clear_buffer = function(bufnr, row) + local namespace = vim.api.nvim_create_namespace "indent_blankline" + local line_start = 0 + local line_end = -1 + if row then + line_start = row - 1 + line_end = row + end + pcall(vim.api.nvim_buf_clear_namespace, bufnr, namespace, line_start, line_end) +end + +---@param config ibl.config +---@param char_map ibl.char_map +---@param whitespace_tbl ibl.indent.whitespace[] +---@param scope_active boolean +---@param scope_index number +---@param scope_end boolean +---@param scope_col_start_single number +---@return ibl.virtual_text, ibl.highlight +M.get = function(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + local scope_hl = utils.tbl_get_index(highlights.scope, scope_index) + local indent_index = 1 + local virt_text = {} + for i, ws in ipairs(whitespace_tbl) do + local whitespace_hl = utils.tbl_get_index(highlights.whitespace, indent_index - 1).char + local indent_hl + local underline_hl + local sa = scope_active + local char = get_char(char_map[ws], indent_index) + + if indent.is_indent(ws) then + whitespace_hl = utils.tbl_get_index(highlights.whitespace, indent_index).char + if vim.fn.strdisplaywidth(char) == 0 then + char = char_map[whitespace.SPACE] --[[@as string]] + sa = false + else + indent_hl = utils.tbl_get_index(highlights.indent, indent_index).char + end + indent_index = indent_index + 1 + end + + if config.scope.show_end and sa and scope_end and i - 1 > scope_col_start_single then + scope_hl = utils.tbl_get_index(highlights.scope, scope_index) + underline_hl = scope_hl.underline + end + + if sa and i - 1 == scope_col_start_single then + indent_hl = scope_hl.char + + if config.scope.char then + local scope_char = get_char(config.scope.char, scope_index) + if vim.fn.strdisplaywidth(scope_char) == 1 then + char = scope_char + end + elseif not indent.is_indent(ws) then + if indent.is_space_indent(ws) then + char = get_char(char_map[whitespace.INDENT], indent_index) + else + char = get_char(char_map[whitespace.TAB_START], indent_index) + end + end + + if config.scope.show_end and scope_end then + underline_hl = scope_hl.underline + end + end + + table.insert(virt_text, { + char, + vim.tbl_filter(function(v) + return v ~= nil + end, { whitespace_hl, indent_hl, underline_hl }), + }) + end + + return virt_text, scope_hl +end + +return M diff --git a/lua/indent_blankline.lua b/lua/indent_blankline.lua new file mode 100644 index 00000000..cbec1a29 --- /dev/null +++ b/lua/indent_blankline.lua @@ -0,0 +1,8 @@ +return { + setup = function() + vim.notify_once( + "You are trying to call the setup function of indent-blankline version 2, but you have version 3 installed.\nTake a look at the readme for instructions on how to migrate, or revert back to version 2.", + vim.log.levels.ERROR + ) + end, +} diff --git a/lua/indent_blankline/commands.lua b/lua/indent_blankline/commands.lua deleted file mode 100644 index 263d3325..00000000 --- a/lua/indent_blankline/commands.lua +++ /dev/null @@ -1,58 +0,0 @@ -local M = {} - -M.refresh = function(bang, scroll) - scroll = scroll or false - if bang then - local win = vim.api.nvim_get_current_win() - vim.cmd(string.format([[noautocmd windo lua require("indent_blankline").refresh(%s)]], tostring(scroll))) - if vim.api.nvim_win_is_valid(win) then - vim.api.nvim_set_current_win(win) - end - else - require("indent_blankline").refresh(scroll) - end -end - -M.enable = function(bang) - if bang then - vim.g.indent_blankline_enabled = true - local win = vim.api.nvim_get_current_win() - vim.cmd [[noautocmd windo lua require("indent_blankline").refresh(false)]] - vim.api.nvim_set_current_win(win) - else - vim.b.indent_blankline_enabled = true - require("indent_blankline").refresh(false) - end -end - -M.disable = function(bang) - if bang then - vim.g.indent_blankline_enabled = false - local buffers = vim.api.nvim_list_bufs() - for _, buffer in pairs(buffers) do - vim.api.nvim_buf_clear_namespace(buffer, vim.g.indent_blankline_namespace, 1, -1) - end - else - vim.b.indent_blankline_enabled = false - vim.b.__indent_blankline_active = false - vim.api.nvim_buf_clear_namespace(0, vim.g.indent_blankline_namespace, 1, -1) - end -end - -M.toggle = function(bang) - if bang then - if vim.g.indent_blankline_enabled then - M.disable(bang) - else - M.enable(bang) - end - else - if vim.b.__indent_blankline_active then - M.disable(bang) - else - M.enable(bang) - end - end -end - -return M diff --git a/lua/indent_blankline/init.lua b/lua/indent_blankline/init.lua deleted file mode 100644 index c45ad8e1..00000000 --- a/lua/indent_blankline/init.lua +++ /dev/null @@ -1,660 +0,0 @@ -local utils = require "indent_blankline/utils" -local M = {} - -local char_highlight = "IndentBlanklineChar" -local space_char_highlight = "IndentBlanklineSpaceChar" -local space_char_blankline_highlight = "IndentBlanklineSpaceCharBlankline" -local context_highlight = "IndentBlanklineContextChar" -local context_space_char_highlight = "IndentBlanklineContextSpaceChar" - -M.init = function() - if not vim.g.indent_blankline_namespace then - vim.g.indent_blankline_namespace = vim.api.nvim_create_namespace "indent_blankline" - end - - utils.reset_highlights() - - require("indent_blankline.commands").refresh(true) -end - -M.setup = function(options) - if options == nil then - options = {} - end - - local o = utils.first_not_nil - - vim.g.indent_blankline_char = o(options.char, vim.g.indent_blankline_char, vim.g.indentLine_char, "│") - vim.g.indent_blankline_char_blankline = o(options.char_blankline, vim.g.indent_blankline_char_blankline) - vim.g.indent_blankline_char_list = - o(options.char_list, vim.g.indent_blankline_char_list, vim.g.indentLine_char_list) - vim.g.indent_blankline_char_list_blankline = - o(options.char_list_blankline, vim.g.indent_blankline_char_list_blankline) - vim.g.indent_blankline_context_char = - o(options.context_char, vim.g.indent_blankline_context_char, vim.g.indent_blankline_char) - vim.g.indent_blankline_context_char_blankline = o( - options.context_char_blankline, - vim.g.indent_blankline_context_char_blankline, - vim.g.indent_blankline_char_blankline - ) - vim.g.indent_blankline_context_char_list = o(options.context_char_list, vim.g.indent_blankline_context_char_list) - vim.g.indent_blankline_context_char_list_blankline = - o(options.context_char_list_blankline, vim.g.indent_blankline_context_char_list) - vim.g.indent_blankline_char_highlight_list = - o(options.char_highlight_list, vim.g.indent_blankline_char_highlight_list) - vim.g.indent_blankline_space_char_highlight_list = - o(options.space_char_highlight_list, vim.g.indent_blankline_space_char_highlight_list) - vim.g.indent_blankline_space_char_blankline = - o(options.space_char_blankline, vim.g.indent_blankline_space_char_blankline, " ") - vim.g.indent_blankline_space_char_blankline_highlight_list = o( - options.space_char_blankline_highlight_list, - vim.g.indent_blankline_space_char_blankline_highlight_list, - options.space_char_highlight_list, - vim.g.indent_blankline_space_char_highlight_list - ) - vim.g.indent_blankline_indent_level = o(options.indent_level, vim.g.indent_blankline_indent_level, 20) - vim.g.indent_blankline_enabled = o(options.enabled, vim.g.indent_blankline_enabled, true) - vim.g.indent_blankline_disable_with_nolist = - o(options.disable_with_nolist, vim.g.indent_blankline_disable_with_nolist, false) - vim.g.indent_blankline_filetype = o(options.filetype, vim.g.indent_blankline_filetype, vim.g.indentLine_fileType) - vim.g.indent_blankline_filetype_exclude = o( - options.filetype_exclude, - vim.g.indent_blankline_filetype_exclude, - vim.g.indentLine_fileTypeExclude, - { "lspinfo", "packer", "checkhealth", "help", "man", "" } - ) - vim.g.indent_blankline_bufname_exclude = - o(options.bufname_exclude, vim.g.indent_blankline_bufname_exclude, vim.g.indentLine_bufNameExclude) - vim.g.indent_blankline_buftype_exclude = o( - options.buftype_exclude, - vim.g.indent_blankline_buftype_exclude, - vim.g.indentLine_bufTypeExclude, - { "terminal", "nofile", "quickfix", "prompt" } - ) - vim.g.indent_blankline_viewport_buffer = o(options.viewport_buffer, vim.g.indent_blankline_viewport_buffer, 10) - vim.g.indent_blankline_use_treesitter = o(options.use_treesitter, vim.g.indent_blankline_use_treesitter, false) - vim.g.indent_blankline_max_indent_increase = o( - options.max_indent_increase, - vim.g.indent_blankline_max_indent_increase, - options.indent_level, - vim.g.indent_blankline_indent_level - ) - vim.g.indent_blankline_show_first_indent_level = - o(options.show_first_indent_level, vim.g.indent_blankline_show_first_indent_level, true) - vim.g.indent_blankline_show_trailing_blankline_indent = - o(options.show_trailing_blankline_indent, vim.g.indent_blankline_show_trailing_blankline_indent, true) - vim.g.indent_blankline_show_end_of_line = - o(options.show_end_of_line, vim.g.indent_blankline_show_end_of_line, false) - vim.g.indent_blankline_show_foldtext = o(options.show_foldtext, vim.g.indent_blankline_show_foldtext, true) - vim.g.indent_blankline_show_current_context = - o(options.show_current_context, vim.g.indent_blankline_show_current_context, false) - vim.g.indent_blankline_show_current_context_start = - o(options.show_current_context_start, vim.g.indent_blankline_show_current_context_start, false) - vim.g.indent_blankline_use_treesitter_scope = - o(options.use_treesitter_scope, vim.g.indent_blankline_use_treesitter_scope, false) - vim.g.indent_blankline_show_current_context_start_on_current_line = o( - options.show_current_context_start_on_current_line, - vim.g.indent_blankline_show_current_context_start_on_current_line, - true - ) - vim.g.indent_blankline_context_highlight_list = - o(options.context_highlight_list, vim.g.indent_blankline_context_highlight_list) - vim.g.indent_blankline_context_patterns = o(options.context_patterns, vim.g.indent_blankline_context_patterns, { - "class", - "^func", - "method", - "^if", - "while", - "for", - "with", - "try", - "except", - "match", - "arguments", - "argument_list", - "object", - "dictionary", - "element", - "table", - "tuple", - "do_block", - "Block", - "InitList", - "FnCallArguments", - "IfStatement", - "ContainerDecl", - "SwitchExpr", - "IfExpr", - "ParamDeclList", - "unless", - }) - vim.g.indent_blankline_context_pattern_highlight = - o(options.context_pattern_highlight, vim.g.indent_blankline_context_pattern_highlight) - vim.g.indent_blankline_strict_tabs = o(options.strict_tabs, vim.g.indent_blankline_strict_tabs, false) - - vim.g.indent_blankline_disable_warning_message = - o(options.disable_warning_message, vim.g.indent_blankline_disable_warning_message, false) - vim.g.indent_blankline_char_priority = o(options.char_priority, vim.g.indent_blankline_char_priority, 1) - vim.g.indent_blankline_context_start_priority = - o(options.context_start_priority, vim.g.indent_blankline_context_start_priority, 10000) - - if vim.g.indent_blankline_show_current_context then - vim.cmd [[ - augroup IndentBlanklineContextAutogroup - autocmd! - autocmd CursorMoved,CursorMovedI * IndentBlanklineRefresh - augroup END - ]] - end - - vim.g.__indent_blankline_setup_completed = true -end - -local refresh = function(scroll) - local v = utils.get_variable - local bufnr = vim.api.nvim_get_current_buf() - - if not vim.api.nvim_buf_is_loaded(bufnr) then - return - end - - if - not utils.is_indent_blankline_enabled( - vim.b.indent_blankline_enabled, - vim.g.indent_blankline_enabled, - v "indent_blankline_disable_with_nolist", - vim.opt.list:get(), - vim.bo.filetype, - v "indent_blankline_filetype" or {}, - v "indent_blankline_filetype_exclude", - vim.bo.buftype, - v "indent_blankline_buftype_exclude" or {}, - v "indent_blankline_bufname_exclude" or {}, - vim.fn["bufname"] "" - ) - then - if vim.b.__indent_blankline_active then - vim.schedule_wrap(utils.clear_buf_indent)(bufnr) - end - vim.b.__indent_blankline_active = false - return - else - vim.b.__indent_blankline_active = true - end - - local win_start = vim.fn.line "w0" - local win_end = vim.fn.line "w$" - local offset = math.max(win_start - 1 - v "indent_blankline_viewport_buffer", 0) - local win_view = vim.fn.winsaveview() - local left_offset = win_view.leftcol - local lnum = win_view.lnum - local left_offset_s = tostring(left_offset) - local range = math.min(win_end + v "indent_blankline_viewport_buffer", vim.api.nvim_buf_line_count(bufnr)) - - if not vim.b.__indent_blankline_ranges then - vim.b.__indent_blankline_ranges = {} - end - - if scroll then - local updated_range - - if vim.b.__indent_blankline_ranges[left_offset_s] then - local blankline_ranges = vim.b.__indent_blankline_ranges[left_offset_s] - local need_to_update = true - - -- find a candidate that could contain the window - local idx_candidate = utils.binary_search_ranges(blankline_ranges, { win_start, win_end }) - local candidate_start, candidate_end = unpack(blankline_ranges[idx_candidate]) - - -- check if the current window is contained or if a new range needs to be created - if candidate_start <= win_start then - if candidate_end >= win_end then - need_to_update = false - else - table.insert(blankline_ranges, idx_candidate + 1, { offset, range }) - end - else - table.insert(blankline_ranges, idx_candidate, { offset, range }) - end - - if not need_to_update then - return - end - - -- merge ranges and update the variable, strategies are: contains or extends - updated_range = utils.merge_ranges(blankline_ranges) - else - updated_range = { { offset, range } } - end - - -- we can't assign directly to a table key, so we update the reference to the variable - local new_ranges = vim.b.__indent_blankline_ranges - new_ranges[left_offset_s] = updated_range - vim.b.__indent_blankline_ranges = new_ranges - else - vim.b.__indent_blankline_ranges = { [left_offset_s] = { { offset, range } } } - end - - local lines = vim.api.nvim_buf_get_lines(bufnr, offset, range, false) - local char = v "indent_blankline_char" - local char_blankline = v "indent_blankline_char_blankline" - local char_list = v "indent_blankline_char_list" or {} - local char_list_blankline = v "indent_blankline_char_list_blankline" or {} - local context_char = v "indent_blankline_context_char" - local context_char_blankline = v "indent_blankline_context_char_blankline" - local context_char_list = v "indent_blankline_context_char_list" or {} - local context_char_list_blankline = v "indent_blankline_context_char_list_blankline" or {} - local char_highlight_list = v "indent_blankline_char_highlight_list" or {} - local space_char_highlight_list = v "indent_blankline_space_char_highlight_list" or {} - local space_char_blankline_highlight_list = v "indent_blankline_space_char_blankline_highlight_list" or {} - local space_char_blankline = v "indent_blankline_space_char_blankline" - local char_priority = v "indent_blankline_char_priority" - local context_start_priority = v "indent_blankline_context_start_priority" - - local list_chars - local no_tab_character = false - -- No need to check for disable_with_nolist as this part would never be executed if "true" && nolist - if vim.opt.list:get() then - -- list is set, get listchars - local tab_characters - local space_character = vim.opt.listchars:get().space or " " - if vim.opt.listchars:get().tab then - -- tab characters can be any UTF-8 character, Lua 5.1 cannot handle this without external libraries - tab_characters = vim.fn.split(vim.opt.listchars:get().tab, "\\zs") - else - no_tab_character = true - tab_characters = { "^", "I" } - end - list_chars = { - space_char = space_character, - trail_char = vim.opt.listchars:get().trail or space_character, - lead_char = vim.opt.listchars:get().lead or space_character, - tab_char_start = tab_characters[1] or space_character, - tab_char_fill = tab_characters[2] or space_character, - tab_char_end = tab_characters[3], - eol_char = vim.opt.listchars:get().eol, - } - else - -- nolist is set, replace all listchars with empty space - list_chars = { - space_char = " ", - trail_char = " ", - lead_char = " ", - tab_char_start = " ", - tab_char_fill = " ", - tab_char_end = nil, - eol_char = nil, - } - end - - local max_indent_level = v "indent_blankline_indent_level" - local max_indent_increase = v "indent_blankline_max_indent_increase" - local expandtab = vim.bo.expandtab - local use_ts_indent = false - local ts_indent - if v "indent_blankline_use_treesitter" then - local ts_query_status, ts_query = pcall(require, "nvim-treesitter.query") - if not ts_query_status then - vim.schedule_wrap(function() - utils.error_handler("nvim-treesitter not found. Treesitter indent will not work", vim.log.levels.WARN) - end)() - end - local ts_indent_status - ts_indent_status, ts_indent = pcall(require, "nvim-treesitter.indent") - use_ts_indent = ts_query_status and ts_indent_status and ts_query.has_indents(vim.bo.filetype) - end - local first_indent = v "indent_blankline_show_first_indent_level" - local trail_indent = v "indent_blankline_show_trailing_blankline_indent" - local end_of_line = v "indent_blankline_show_end_of_line" - local strict_tabs = v "indent_blankline_strict_tabs" - local foldtext = v "indent_blankline_show_foldtext" - - local tabs = vim.bo.shiftwidth == 0 or not expandtab - local shiftwidth = utils._if(tabs, utils._if(no_tab_character, 2, vim.bo.tabstop), vim.bo.shiftwidth) - - local context_highlight_list = v "indent_blankline_context_highlight_list" or {} - local context_pattern_highlight = v "indent_blankline_context_pattern_highlight" or {} - local context_status, context_start, context_end, context_pattern = false, 0, 0, nil - local show_current_context_start = v "indent_blankline_show_current_context_start" - local show_current_context_start_on_current_line = v "indent_blankline_show_current_context_start_on_current_line" - if v "indent_blankline_show_current_context" then - context_status, context_start, context_end, context_pattern = - utils.get_current_context(v "indent_blankline_context_patterns", v "indent_blankline_use_treesitter_scope") - end - - local get_virtual_text = - function(indent, extra, blankline, context_active, context_indent, prev_indent, virtual_string) - local virtual_text = {} - local current_left_offset = left_offset - local local_max_indent_level = math.min(max_indent_level, prev_indent + max_indent_increase) - local indent_char = utils._if(blankline and char_blankline, char_blankline, char) - local context_indent_char = - utils._if(blankline and context_char_blankline, context_char_blankline, context_char) - local indent_char_list = utils._if(blankline and #char_list_blankline > 0, char_list_blankline, char_list) - local context_indent_char_list = utils._if( - blankline and #context_char_list_blankline > 0, - context_char_list_blankline, - context_char_list - ) - for i = 1, math.min(math.max(indent, 0), local_max_indent_level) do - local space_count = shiftwidth - local context = context_active and context_indent == i - local show_indent_char = (i ~= 1 or first_indent) and indent_char ~= "" - local show_context_indent_char = context and (i ~= 1 or first_indent) and context_indent_char ~= "" - local show_end_of_line_char = i == 1 and blankline and end_of_line and list_chars["eol_char"] - local show_indent_or_eol_char = show_indent_char or show_context_indent_char or show_end_of_line_char - if show_indent_or_eol_char then - space_count = space_count - 1 - if current_left_offset > 0 then - current_left_offset = current_left_offset - 1 - else - table.insert(virtual_text, { - utils._if( - show_end_of_line_char, - list_chars["eol_char"], - utils._if( - context, - utils.get_from_list( - context_indent_char_list, - i - utils._if(not first_indent, 1, 0), - context_indent_char - ), - utils.get_from_list( - indent_char_list, - i - utils._if(not first_indent, 1, 0), - indent_char - ) - ) - ), - utils._if( - context, - utils._if( - context_pattern_highlight[context_pattern], - context_pattern_highlight[context_pattern], - utils.get_from_list(context_highlight_list, i, context_highlight) - ), - utils.get_from_list(char_highlight_list, i, char_highlight) - ), - }) - end - end - if current_left_offset > 0 then - local current_space_count = space_count - space_count = space_count - current_left_offset - current_left_offset = current_left_offset - current_space_count - end - if space_count > 0 then - -- ternary operator below in table.insert() doesn't work because it would evaluate each option regardless - local tmp_string - local index = 1 + (i - 1) * shiftwidth - if show_indent_or_eol_char then - if table.maxn(virtual_string) >= index + space_count then - -- first char was already set above - tmp_string = table.concat(virtual_string, "", index + 1, index + space_count) - end - else - if table.maxn(virtual_string) >= index + space_count - 1 then - tmp_string = table.concat(virtual_string, "", index, index + space_count - 1) - end - end - table.insert(virtual_text, { - utils._if( - tmp_string, - tmp_string, - utils._if(blankline, space_char_blankline, list_chars["lead_char"]):rep(space_count) - ), - utils._if( - blankline, - utils.get_from_list(space_char_blankline_highlight_list, i, space_char_blankline_highlight), - utils.get_from_list( - space_char_highlight_list, - i, - utils._if(context, context_space_char_highlight, space_char_highlight) - ) - ), - }) - end - end - - local index = math.ceil(#virtual_text / 2) + 1 - local extra_context_active = context_active and context_indent == index - - if - ( - (indent_char ~= "" or #indent_char_list > 0) - or (extra_context_active and (context_indent_char ~= "" or #context_char_list > 0)) - ) - and ((blankline or extra) and trail_indent) - and (first_indent or #virtual_text > 0) - and current_left_offset < 1 - and indent < local_max_indent_level - then - table.insert(virtual_text, { - utils._if( - extra_context_active, - utils.get_from_list( - context_indent_char_list, - index - utils._if(not first_indent, 1, 0), - context_indent_char - ), - utils.get_from_list(indent_char_list, index - utils._if(not first_indent, 1, 0), indent_char) - ), - utils._if( - extra_context_active, - utils.get_from_list(context_highlight_list, index, context_highlight), - utils.get_from_list(char_highlight_list, index, char_highlight) - ), - }) - end - - return virtual_text - end - - local prev_indent - local next_indent - local next_extra - local empty_line_counter = 0 - local context_indent - for i = 1, #lines do - if foldtext and vim.fn.foldclosed(i + offset) > 0 then - utils.clear_line_indent(bufnr, i + offset) - else - local async - async = vim.loop.new_async(function() - local blankline = lines[i]:len() == 0 - local whitespace = string.match(lines[i], "^%s+") or "" - local only_whitespace = whitespace == lines[i] - local context_active = false - local context_first_line = false - if context_status then - context_active = offset + i > context_start and offset + i <= context_end - context_first_line = offset + i == context_start - end - - if blankline and use_ts_indent then - vim.schedule_wrap(function() - local indent = ts_indent.get_indent(i + offset) or 0 - utils.clear_line_indent(bufnr, i + offset) - - if - context_first_line - and show_current_context_start - and (show_current_context_start_on_current_line or lnum ~= context_start) - then - if - not vim.api.nvim_buf_is_loaded(bufnr) - or not vim.api.nvim_buf_get_var(bufnr, "__indent_blankline_active") - then - return - end - xpcall( - vim.api.nvim_buf_set_extmark, - utils.error_handler, - bufnr, - vim.g.indent_blankline_namespace, - context_start - 1, - #whitespace, - { - end_col = #lines[i], - hl_group = "IndentBlanklineContextStart", - priority = context_start_priority, - } - ) - end - - if indent == 0 then - return - end - - indent = indent / shiftwidth - if context_first_line then - context_indent = indent + 1 - end - - local virtual_text = get_virtual_text( - indent, - false, - blankline, - context_active, - context_indent, - max_indent_level, - {} - ) - if - not vim.api.nvim_buf_is_loaded(bufnr) - or not vim.api.nvim_buf_get_var(bufnr, "__indent_blankline_active") - then - return - end - xpcall( - vim.api.nvim_buf_set_extmark, - utils.error_handler, - bufnr, - vim.g.indent_blankline_namespace, - i - 1 + offset, - 0, - { - virt_text = virtual_text, - virt_text_pos = "overlay", - hl_mode = "combine", - priority = char_priority, - } - ) - end)() - return async:close() - end - - local indent, extra - local virtual_string = {} - if not blankline then - indent, extra, virtual_string = - utils.find_indent(whitespace, only_whitespace, shiftwidth, strict_tabs, list_chars) - elseif empty_line_counter > 0 then - empty_line_counter = empty_line_counter - 1 - indent = next_indent - extra = next_extra - else - if i == #lines then - indent = 0 - extra = false - else - local j = i + 1 - while j < #lines and lines[j]:len() == 0 do - j = j + 1 - empty_line_counter = empty_line_counter + 1 - end - local j_whitespace = string.match(lines[j], "^%s+") - local j_only_whitespace = j_whitespace == lines[j] - indent, extra, _ = - utils.find_indent(j_whitespace, j_only_whitespace, shiftwidth, strict_tabs, list_chars) - end - next_indent = indent - next_extra = extra - end - - if context_first_line then - context_indent = indent + 1 - end - - vim.schedule_wrap(utils.clear_line_indent)(bufnr, i + offset) - if - context_first_line - and show_current_context_start - and (show_current_context_start_on_current_line or lnum ~= context_start) - then - vim.schedule_wrap(function() - if - not vim.api.nvim_buf_is_loaded(bufnr) - or not vim.api.nvim_buf_get_var(bufnr, "__indent_blankline_active") - then - return - end - xpcall( - vim.api.nvim_buf_set_extmark, - utils.error_handler, - bufnr, - vim.g.indent_blankline_namespace, - context_start - 1, - #whitespace, - { - end_col = #lines[i], - hl_group = "IndentBlanklineContextStart", - priority = context_start_priority, - } - ) - end)() - end - - if indent == 0 and #virtual_string == 0 and not extra then - prev_indent = 0 - return async:close() - end - - if not prev_indent or indent + utils._if(extra, 1, 0) <= prev_indent + max_indent_increase then - prev_indent = indent - end - - local virtual_text = get_virtual_text( - indent, - extra, - blankline, - context_active, - context_indent, - prev_indent - utils._if(trail_indent, 0, 1), - virtual_string - ) - vim.schedule_wrap(function() - if - not vim.api.nvim_buf_is_loaded(bufnr) - or not vim.api.nvim_buf_get_var(bufnr, "__indent_blankline_active") - then - return - end - xpcall( - vim.api.nvim_buf_set_extmark, - utils.error_handler, - bufnr, - vim.g.indent_blankline_namespace, - i - 1 + offset, - 0, - { - virt_text = virtual_text, - virt_text_pos = "overlay", - hl_mode = "combine", - priority = char_priority, - } - ) - end)() - return async:close() - end) - - async:send() - end - end -end - -M.refresh = function(scroll) - xpcall(refresh, utils.error_handler, scroll) -end - -return M diff --git a/lua/indent_blankline/utils.lua b/lua/indent_blankline/utils.lua deleted file mode 100644 index 0e609d04..00000000 --- a/lua/indent_blankline/utils.lua +++ /dev/null @@ -1,343 +0,0 @@ -local M = {} - -M.memo = setmetatable({ - put = function(cache, params, result) - local node = cache - for i = 1, #params do - local param = vim.inspect(params[i]) - node.children = node.children or {} - node.children[param] = node.children[param] or {} - node = node.children[param] - end - node.result = result - end, - get = function(cache, params) - local node = cache - for i = 1, #params do - local param = vim.inspect(params[i]) - node = node.children and node.children[param] - if not node then - return nil - end - end - return node.result - end, -}, { - __call = function(memo, func) - local cache = {} - - return function(...) - local params = { ... } - local result = memo.get(cache, params) - if not result then - result = { func(...) } - memo.put(cache, params, result) - end - return unpack(result) - end - end, -}) - -M.error_handler = function(err, level) - if err:match "Invalid buffer id.*" then - return - end - if not pcall(require, "notify") then - err = string.format("indent-blankline: %s", err) - end - vim.notify_once(err, level or vim.log.levels.DEBUG, { - title = "indent-blankline", - }) -end - -M.is_indent_blankline_enabled = M.memo( - function( - b_enabled, - g_enabled, - disable_with_nolist, - opt_list, - filetype, - filetype_include, - filetype_exclude, - buftype, - buftype_exclude, - bufname_exclude, - bufname - ) - if b_enabled ~= nil then - return b_enabled - end - if g_enabled ~= true then - return false - end - if disable_with_nolist and not opt_list then - return false - end - - local plain = M._if(vim.fn.has "nvim-0.6.0" == 1, { plain = true }, true) - local undotted_filetypes = vim.split(filetype, ".", plain) - table.insert(undotted_filetypes, filetype) - - for _, ft in ipairs(filetype_exclude) do - for _, undotted_filetype in ipairs(undotted_filetypes) do - if undotted_filetype == ft then - return false - end - end - end - - for _, bt in ipairs(buftype_exclude) do - if bt == buftype then - return false - end - end - - for _, bn in ipairs(bufname_exclude) do - if vim.fn["matchstr"](bufname, bn) == bufname then - return false - end - end - - if #filetype_include > 0 then - for _, ft in ipairs(filetype_include) do - if ft == filetype then - return true - end - end - return false - end - - return true - end -) - -M.clear_line_indent = function(buf, lnum) - xpcall(vim.api.nvim_buf_clear_namespace, M.error_handler, buf, vim.g.indent_blankline_namespace, lnum - 1, lnum) -end - -M.clear_buf_indent = function(buf) - xpcall(vim.api.nvim_buf_clear_namespace, M.error_handler, buf, vim.g.indent_blankline_namespace, 0, -1) -end - -M.get_from_list = function(list, i, default) - if not list or #list == 0 then - return default - end - return list[((i - 1) % #list) + 1] -end - -M._if = function(bool, a, b) - if bool then - return a - else - return b - end -end - -M.find_indent = function(whitespace, only_whitespace, shiftwidth, strict_tabs, list_chars) - local indent = 0 - local spaces = 0 - local tab_width - local virtual_string = {} - - if whitespace then - for ch in whitespace:gmatch "." do - if ch == "\t" then - if strict_tabs and indent == 0 and spaces ~= 0 then - return 0, false, {} - end - indent = indent + math.floor(spaces / shiftwidth) + 1 - spaces = 0 - -- replace dynamic-width tab with fixed-width string (ta..ab) - tab_width = shiftwidth - table.maxn(virtual_string) % shiftwidth - -- check if tab_char_end is set, see :help listchars - if list_chars["tab_char_end"] then - if tab_width == 1 then - table.insert(virtual_string, list_chars["tab_char_end"]) - else - table.insert(virtual_string, list_chars["tab_char_start"]) - for _ = 1, (tab_width - 2) do - table.insert(virtual_string, list_chars["tab_char_fill"]) - end - table.insert(virtual_string, list_chars["tab_char_end"]) - end - else - table.insert(virtual_string, list_chars["tab_char_start"]) - for _ = 1, (tab_width - 1) do - table.insert(virtual_string, list_chars["tab_char_fill"]) - end - end - else - if strict_tabs and indent ~= 0 then - -- return early when no more tabs are found - return indent, true, virtual_string - end - if only_whitespace then - -- if the entire line is only whitespace use trail_char instead of lead_char - table.insert(virtual_string, list_chars["trail_char"]) - else - table.insert(virtual_string, list_chars["lead_char"]) - end - spaces = spaces + 1 - end - end - end - - return indent + math.floor(spaces / shiftwidth), table.maxn(virtual_string) % shiftwidth ~= 0, virtual_string -end - -M.get_current_context = function(type_patterns, use_treesitter_scope) - local ts_utils_status, ts_utils = pcall(require, "nvim-treesitter.ts_utils") - if not ts_utils_status then - vim.schedule_wrap(function() - M.error_handler("nvim-treesitter not found. Context will not work", vim.log.levels.WARN) - end)() - return false - end - local locals = require "nvim-treesitter.locals" - local cursor_node = ts_utils.get_node_at_cursor() - - if use_treesitter_scope then - local current_scope = locals.containing_scope(cursor_node, 0) - if not current_scope then - return false - end - local node_start, _, node_end, _ = current_scope:range() - if node_start == node_end then - return false - end - return true, node_start + 1, node_end + 1, current_scope:type() - end - - while cursor_node do - local node_type = cursor_node:type() - for _, rgx in ipairs(type_patterns) do - if node_type:find(rgx) then - local node_start, _, node_end, _ = cursor_node:range() - if node_start ~= node_end then - return true, node_start + 1, node_end + 1, rgx - end - end - end - cursor_node = cursor_node:parent() - end - - return false -end - -M.reset_highlights = function() - local whitespace_highlight = vim.fn.synIDtrans(vim.fn.hlID "Whitespace") - local label_highlight = vim.fn.synIDtrans(vim.fn.hlID "Label") - - local whitespace_fg = { - vim.fn.synIDattr(whitespace_highlight, "fg", "gui"), - vim.fn.synIDattr(whitespace_highlight, "fg", "cterm"), - } - local label_fg = { - vim.fn.synIDattr(label_highlight, "fg", "gui"), - vim.fn.synIDattr(label_highlight, "fg", "cterm"), - } - - for highlight_name, highlight in pairs { - IndentBlanklineChar = whitespace_fg, - IndentBlanklineSpaceChar = whitespace_fg, - IndentBlanklineSpaceCharBlankline = whitespace_fg, - IndentBlanklineContextChar = label_fg, - IndentBlanklineContextStart = label_fg, - } do - local current_highlight = vim.fn.synIDtrans(vim.fn.hlID(highlight_name)) - if - vim.fn.synIDattr(current_highlight, "fg") == "" - and vim.fn.synIDattr(current_highlight, "bg") == "" - and vim.fn.synIDattr(current_highlight, "sp") == "" - and vim.fn.synIDattr(current_highlight, "fg", "cterm") == "" - and vim.fn.synIDattr(current_highlight, "bg", "cterm") == "" - then - if highlight_name == "IndentBlanklineContextStart" then - vim.cmd( - string.format( - "highlight %s guisp=%s gui=underline cterm=underline", - highlight_name, - M._if(highlight[1] == "", "NONE", highlight[1]) - ) - ) - else - vim.cmd( - string.format( - "highlight %s guifg=%s ctermfg=%s gui=nocombine cterm=nocombine", - highlight_name, - M._if(highlight[1] == "", "NONE", highlight[1]), - M._if(highlight[2] == "", "NONE", highlight[2]) - ) - ) - end - end - end -end - -M.first_not_nil = function(...) - for _, value in pairs { ... } do -- luacheck: ignore - return value - end -end - -M.get_variable = function(key) - if vim.b[key] ~= nil then - return vim.b[key] - end - if vim.t[key] ~= nil then - return vim.t[key] - end - return vim.g[key] -end - -M.merge_ranges = function(ranges) - local merged_ranges = { { unpack(ranges[1]) } } - - for i = 2, #ranges do - local current_end = merged_ranges[#merged_ranges][2] - local next_start, next_end = unpack(ranges[i]) - if current_end >= next_start - 1 then - if current_end < next_end then - merged_ranges[#merged_ranges][2] = next_end - end - else - table.insert(merged_ranges, { next_start, next_end }) - end - end - - return merged_ranges -end - -M.binary_search_ranges = function(ranges, target_range) - local exact_match = false - local idx_start = 1 - local idx_end = #ranges - local idx_mid - - local range_start - local target_start = target_range[1] - - while idx_start < idx_end do - idx_mid = math.ceil((idx_start + idx_end) / 2) - range_start = ranges[idx_mid][1] - - if range_start == target_start then - exact_match = true - break - elseif range_start < target_start then - idx_start = idx_mid -- it's important to make the low-end inclusive - else - idx_end = idx_mid - 1 - end - end - - -- if we don't have an exact match, choose the smallest index - if not exact_match then - idx_mid = idx_start - end - - return idx_mid -end - -return M diff --git a/plugin/indent_blankline.vim b/plugin/indent_blankline.vim deleted file mode 100644 index 203d140a..00000000 --- a/plugin/indent_blankline.vim +++ /dev/null @@ -1,41 +0,0 @@ - -if exists('g:loaded_indent_blankline') || !has('nvim-0.5.0') - finish -endif -let g:loaded_indent_blankline = 1 - -function s:try(cmd) - try - execute a:cmd - catch /E12/ - return - endtry -endfunction - -command! -bang IndentBlanklineRefresh call s:try('lua require("indent_blankline.commands").refresh("" == "!")') -command! -bang IndentBlanklineRefreshScroll call s:try('lua require("indent_blankline.commands").refresh("" == "!", true)') -command! -bang IndentBlanklineEnable call s:try('lua require("indent_blankline.commands").enable("" == "!")') -command! -bang IndentBlanklineDisable call s:try('lua require("indent_blankline.commands").disable("" == "!")') -command! -bang IndentBlanklineToggle call s:try('lua require("indent_blankline.commands").toggle("" == "!")') - -if exists(':IndentLinesEnable') && !g:indent_blankline_disable_warning_message - echohl Error - echom 'indent-blankline does not require IndentLine anymore, please remove it.' - echohl None -endif - -if !exists('g:__indent_blankline_setup_completed') - lua require("indent_blankline").setup {} -endif - -lua require("indent_blankline").init() - -augroup IndentBlanklineAutogroup - autocmd! - autocmd OptionSet list,listchars,shiftwidth,tabstop,expandtab IndentBlanklineRefresh - autocmd FileChangedShellPost,TextChanged,TextChangedI,CompleteChanged,BufWinEnter,Filetype * IndentBlanklineRefresh - autocmd WinScrolled * IndentBlanklineRefreshScroll - autocmd ColorScheme * lua require("indent_blankline.utils").reset_highlights() - autocmd VimEnter * lua require("indent_blankline").init() -augroup END - diff --git a/specs/features/config_spec.lua b/specs/features/config_spec.lua new file mode 100644 index 00000000..0858666a --- /dev/null +++ b/specs/features/config_spec.lua @@ -0,0 +1,158 @@ +assert = require "luassert" +local conf = require "ibl.config" + +describe("set_config", function() + before_each(function() + conf.set_config() + end) + + it("fills in values with the default config", function() + local config = conf.set_config() + + assert.are.same(config, conf.default_config) + end) + + it("uses the passed config", function() + local config = conf.set_config { enabled = false } + + assert.are_not.equal(config.enabled, conf.default_config.enabled) + end) + + it("validates the passed config", function() + ---@diagnostic disable-next-line + local ok = pcall(conf.set_config, { enabled = "string" }) + + assert.are.equal(ok, false) + end) + + it("resets the config every time", function() + conf.set_config { enabled = false } + local config = conf.set_config { debounce = 100 } + + assert.are.equal(config.enabled, true) + assert.are.equal(config.debounce, 100) + end) + + it("merges passed in lists", function() + local config = conf.set_config { exclude = { buftypes = { "foo" } } } + + assert.are.equal(vim.tbl_contains(config.exclude.buftypes, "foo"), true) + assert.are.equal(vim.tbl_contains(config.exclude.buftypes, "terminal"), true) + end) + + it("merges node_type", function() + local config = conf.set_config { + scope = { + exclude = { + node_type = { + foo = { "a", "b" }, + lua = { "c" }, + }, + }, + }, + } + + assert.are.equal(vim.tbl_contains(config.scope.exclude.node_type.foo, "a"), true) + assert.are.equal(vim.tbl_contains(config.scope.exclude.node_type.foo, "b"), true) + assert.are.equal(vim.tbl_contains(config.scope.exclude.node_type.lua, "c"), true) + assert.are.equal(vim.tbl_contains(config.scope.exclude.node_type.lua, "chunk"), true) + end) +end) + +describe("update_config", function() + before_each(function() + conf.set_config() + end) + + it("updates the existing config", function() + conf.set_config { enabled = false } + local config = conf.update_config { debounce = 100 } + + assert.are.equal(config.enabled, false) + assert.are.equal(config.debounce, 100) + end) +end) + +describe("overwrite_config", function() + before_each(function() + conf.set_config() + end) + + it("overwrites passed in lists", function() + local config = conf.overwrite_config { exclude = { buftypes = { "foo" } } } + + assert.are.equal(vim.tbl_contains(config.exclude.buftypes, "foo"), true) + assert.are.equal(vim.tbl_contains(config.exclude.buftypes, "terminal"), false) + end) +end) + +describe("set_buffer_config", function() + local bufnr = 99 + before_each(function() + conf.set_config() + conf.clear_buffer_config(bufnr) + end) + + it("uses the passed config", function() + local config = conf.set_buffer_config(bufnr, { enabled = false }) + + assert.are_not.equal(config.enabled, conf.default_config.enabled) + end) + + it("uses uses the current global config as the default", function() + conf.set_config { debounce = 100 } + local config = conf.set_buffer_config(bufnr, { enabled = false }) + + assert.are.equal(config.debounce, 100) + assert.are.equal(config.enabled, false) + end) + + it("validates the passed config", function() + ---@diagnostic disable-next-line + local ok = pcall(conf.set_buffer_config, bufnr, { enabled = "string" }) + + assert.are.equal(ok, false) + end) + + it("resets the config every time", function() + conf.set_buffer_config(bufnr, { enabled = false }) + local config = conf.set_buffer_config(bufnr, { debounce = 100 }) + + assert.are.equal(config.enabled, true) + assert.are.equal(config.debounce, 100) + end) +end) + +describe("get_config", function() + local bufnr = 99 + + before_each(function() + conf.set_config() + conf.clear_buffer_config(bufnr) + end) + + it("gets the global config by default", function() + local config = conf.get_config(bufnr) + + assert.are.same(config, conf.default_config) + end) + + it("gets the buffer config if available", function() + conf.set_buffer_config(bufnr, { enabled = false }) + local config = conf.get_config(bufnr) + + assert.are.equal(config.enabled, false) + end) + + it( + "falls back to the global config if a value is not in the buffer config, even if it changed after the buffer config was set", + function() + conf.set_buffer_config(bufnr, { enabled = false }) + conf.set_config { debounce = 100 } + local config = conf.get_config(bufnr) + + assert.are.equal(config.enabled, false) + assert.are.equal(config.debounce, 100) + end + ) +end) diff --git a/specs/features/hooks_spec.lua b/specs/features/hooks_spec.lua new file mode 100644 index 00000000..427e0c9c --- /dev/null +++ b/specs/features/hooks_spec.lua @@ -0,0 +1,83 @@ +assert = require "luassert" +local hooks = require "ibl.hooks" + +describe("hooks", function() + before_each(function() + hooks.clear_all() + end) + + it("registers a new hook", function() + local hook_id = hooks.register(hooks.type.ACTIVE, function() + return true + end) + + assert.is.equal(type(hook_id), "string") + end) + + it("does not allow invalid types", function() + local ok, _ = pcall(hooks.register, "invalid", function() + return true + end) + assert.is.False(ok) + end) + + it("does not allow nil types", function() + local ok, _ = pcall(hooks.register, nil, function() + return true + end) + assert.is.False(ok) + end) + + it("does not allow invalid function", function() + local ok, _ = pcall(hooks.register, hooks.type.ACTIVE, nil) + assert.is.False(ok) + end) + + it("registers hooks globally by default", function() + hooks.register(hooks.type.ACTIVE, function() + return true + end) + + assert.equal(#hooks.get(9999, hooks.type.ACTIVE), 1) + end) + + it("registers hooks to buffer when bufnr ~= nil", function() + hooks.register(hooks.type.ACTIVE, function() + return true + end, { bufnr = 1 }) + + assert.equal(#hooks.get(1, hooks.type.ACTIVE), 1) + assert.equal(#hooks.get(9999, hooks.type.ACTIVE), 0) + end) + + it("registers hooks to the current buffer when bufnr == 0", function() + local bufnr = vim.api.nvim_get_current_buf() + hooks.register(hooks.type.ACTIVE, function() + return true + end, { bufnr = 0 }) + + assert.equal(#hooks.get(bufnr, hooks.type.ACTIVE), 1) + end) +end) + +describe("default hooks", function() + describe("skip_preproc_lines", function() + local skip_preproc_lines = hooks.builtin.skip_preproc_lines + + it("does not match 'foo'", function() + assert.is.False(skip_preproc_lines(0, 0, 0, "foo")) + end) + + it("does match '#if'", function() + assert.is.True(skip_preproc_lines(0, 0, 0, "#if")) + end) + + it("does match with trailing whitespace", function() + assert.is.False(skip_preproc_lines(0, 0, 0, " #if")) + end) + + it("does match '#if something'", function() + assert.is.True(skip_preproc_lines(0, 0, 0, "#if something")) + end) + end) +end) diff --git a/specs/features/indent_spec.lua b/specs/features/indent_spec.lua new file mode 100644 index 00000000..6c451987 --- /dev/null +++ b/specs/features/indent_spec.lua @@ -0,0 +1,94 @@ +assert = require "luassert" +local indent = require "ibl.indent" +local TAB_START = indent.whitespace.TAB_START +local TAB_START_SINGLE = indent.whitespace.TAB_START_SINGLE +local TAB_FILL = indent.whitespace.TAB_FILL +local TAB_END = indent.whitespace.TAB_END +local SPACE = indent.whitespace.SPACE +local INDENT = indent.whitespace.INDENT + +describe("indent", function() + local opts + before_each(function() + opts = { + tabstop = 4, + vartabstop = "", + shiftwidth = 2, + smart_indent_cap = true, + } + end) + + it("no whitespace", function() + local whitespace_tbl, _ = indent.get("", opts) + + assert.are.same(whitespace_tbl, {}) + end) + + it("normal space indentation", function() + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same(whitespace_tbl, { INDENT, SPACE }) + end) + + it("normal tab", function() + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same(whitespace_tbl, { TAB_START, TAB_FILL, TAB_FILL, TAB_END }) + end) + + it("single width tab", function() + opts.tabstop = 1 + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same(whitespace_tbl, { TAB_START_SINGLE }) + end) + + it("double width tab", function() + opts.tabstop = 2 + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same(whitespace_tbl, { TAB_START, TAB_END }) + end) + + it("vartabstop", function() + opts.vartabstop = "1,3" + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same( + whitespace_tbl, + { TAB_START_SINGLE, TAB_START, TAB_FILL, TAB_END, TAB_START, TAB_FILL, TAB_END } + ) + end) + + it("mix of tabs and spaces", function() + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same(whitespace_tbl, { INDENT, SPACE, TAB_START, TAB_END, SPACE, TAB_START_SINGLE }) + end) + + it("mix of tabs and spaces with vartabstop", function() + opts.vartabstop = "1,3" + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + + assert.are.same(whitespace_tbl, { TAB_START_SINGLE, SPACE, TAB_START, TAB_END, SPACE }) + end) + + it("caps after first indent after last item in stack", function() + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0, 4 } }) + + assert.are.same(whitespace_tbl, { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE, SPACE, SPACE }) + end) + + it("caps after first indent of first item in stack when cap is true", function() + local whitespace_tbl, _ = indent.get(" ", opts, { cap = true, stack = { 0, 4 } }) + + assert.are.same(whitespace_tbl, { INDENT, SPACE, SPACE, SPACE, INDENT, SPACE, SPACE, SPACE }) + end) + + it("doesn't cap with smart_indent_cap off", function() + opts.smart_indent_cap = false + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0, 4 } }) + + assert.are.same(whitespace_tbl, { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE, INDENT, SPACE }) + end) +end) diff --git a/specs/features/utils_spec.lua b/specs/features/utils_spec.lua new file mode 100644 index 00000000..3544f247 --- /dev/null +++ b/specs/features/utils_spec.lua @@ -0,0 +1,52 @@ +assert = require "luassert" +local utils = require "ibl.utils" + +describe("get_listchars", function() + local listchars = vim.opt.listchars:get() + + after_each(function() + vim.opt.listchars = listchars + vim.opt.list = false + end) + + it("returns fallback listchars if list is off", function() + assert.are.same(utils.get_listchars(0), { + tabstop_overwrite = false, + space_char = " ", + tab_char_fill = " ", + }) + end) + + it("returns default listchars", function() + vim.opt.list = true + assert.are.same(utils.get_listchars(0), { + tabstop_overwrite = false, + space_char = " ", + tab_char_start = ">", + tab_char_fill = " ", + trail_char = "-", + }) + end) + + it("sets tabstop_overwrite to true when there is are tab chars", function() + vim.opt.list = true + vim.opt.listchars = {} + assert.are.same(utils.get_listchars(0), { + tabstop_overwrite = true, + space_char = " ", + tab_char_start = "^", + tab_char_fill = "I", + }) + end) + + it("splits utf-8 chars correctly", function() + vim.opt.list = true + vim.opt.listchars = { tab = "󱢗󰗲" } + assert.are.same(utils.get_listchars(0), { + tabstop_overwrite = false, + space_char = " ", + tab_char_start = "󱢗", + tab_char_fill = "󰗲", + }) + end) +end) diff --git a/specs/features/virt_text_spec.lua b/specs/features/virt_text_spec.lua new file mode 100644 index 00000000..2e39c927 --- /dev/null +++ b/specs/features/virt_text_spec.lua @@ -0,0 +1,565 @@ +assert = require "luassert" +local conf = require "ibl.config" +local indent = require "ibl.indent" +local whitespace = indent.whitespace +local highlights = require "ibl.highlights" +local vt = require "ibl.virt_text" + +local TAB_START = whitespace.TAB_START +local TAB_START_SINGLE = whitespace.TAB_START_SINGLE +local TAB_FILL = whitespace.TAB_FILL +local TAB_END = whitespace.TAB_END +local SPACE = whitespace.SPACE +local INDENT = whitespace.INDENT + +describe("get_char_map", function() + before_each(function() + conf.set_config() + end) + + it("makes a basic char map", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = nil, + lead_char = nil, + multispace_chars = nil, + leadmultispace_chars = nil, + tab_char_start = ">", + tab_char_fill = " ", + tab_char_end = nil, + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = ">", + [TAB_START_SINGLE] = ">", + [TAB_FILL] = " ", + [TAB_END] = " ", + [SPACE] = " ", + [INDENT] = "i", + }) + end) + + it("uses tab_char for tabs", function() + local config = conf.set_config { indent = { char = "i", tab_char = "t" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = nil, + lead_char = nil, + multispace_chars = nil, + leadmultispace_chars = nil, + tab_char_start = nil, + tab_char_fill = " ", + tab_char_end = nil, + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "t", + [TAB_START_SINGLE] = "t", + [TAB_FILL] = " ", + [TAB_END] = " ", + [SPACE] = " ", + [INDENT] = "i", + }) + end) + + it("uses char for tabs if everything else is nil", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = nil, + lead_char = nil, + multispace_chars = nil, + leadmultispace_chars = nil, + tab_char_start = nil, + tab_char_fill = " ", + tab_char_end = nil, + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "i", + [TAB_START_SINGLE] = "i", + [TAB_FILL] = " ", + [TAB_END] = " ", + [SPACE] = " ", + [INDENT] = "i", + }) + end) + + it("parses basic listchars", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = "w", + lead_char = "a", + multispace_chars = nil, + leadmultispace_chars = nil, + tab_char_start = "b", + tab_char_fill = "c", + tab_char_end = "d", + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "b", + [TAB_START_SINGLE] = "d", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "a", + [INDENT] = "i", + }) + end) + + it("parses uses multispace listchars", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = "w", + lead_char = nil, + multispace_chars = { "x", "y" }, + leadmultispace_chars = nil, + tab_char_start = "b", + tab_char_fill = "c", + tab_char_end = "d", + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "b", + [TAB_START_SINGLE] = "d", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = { "x", "y" }, + [INDENT] = "i", + }) + end) + + it("uses lead over multispace listchars", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = "w", + lead_char = "a", + multispace_chars = { "x", "y" }, + leadmultispace_chars = nil, + tab_char_start = "b", + tab_char_fill = "c", + tab_char_end = "d", + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "b", + [TAB_START_SINGLE] = "d", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "a", + [INDENT] = "i", + }) + end) + + it("uses leadmultispace over lead listchars", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = "w", + lead_char = "a", + multispace_chars = { "x", "y" }, + leadmultispace_chars = { "o", "i" }, + tab_char_start = "b", + tab_char_fill = "c", + tab_char_end = "d", + } + local whitespace_only = false + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "b", + [TAB_START_SINGLE] = "d", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = { "o", "i" }, + [INDENT] = "i", + }) + end) + + it("uses trail listchars on whitspace only lines", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = " ", + trail_char = "w", + lead_char = "a", + multispace_chars = { "x", "y" }, + leadmultispace_chars = { "o", "i" }, + tab_char_start = "b", + tab_char_fill = "c", + tab_char_end = "d", + } + local whitespace_only = true + local blankline = false + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "b", + [TAB_START_SINGLE] = "d", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "w", + [INDENT] = "i", + }) + end) + + it("uses spaces on blanklines", function() + local config = conf.set_config { indent = { char = "i" } } + local listchars = { + tabstop_overwrite = false, + space_char = "s", + trail_char = "w", + lead_char = "a", + multispace_chars = { "x", "y" }, + leadmultispace_chars = { "o", "i" }, + tab_char_start = "b", + tab_char_fill = "c", + tab_char_end = "d", + } + local whitespace_only = false + local blankline = true + local char_map = vt.get_char_map(config, listchars, whitespace_only, blankline) + + assert.are.same(char_map, { + [TAB_START] = "b", + [TAB_START_SINGLE] = "d", + [TAB_FILL] = " ", + [TAB_END] = " ", + [SPACE] = " ", + [INDENT] = "i", + }) + end) +end) + +describe("virt_text", function() + before_each(function() + conf.set_config() + end) + + it("handles empty whitespace table", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = {} + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, {}) + end) + + it("handles simple indentation", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE } + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles a list of indent chars", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = "o", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = { "a", "b", "c" }, + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "a", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "b", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "c", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles a list of tab chars", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = { "a", "b", "c" }, + [TAB_START_SINGLE] = "o", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { TAB_START, TAB_END, TAB_START, TAB_END, TAB_START, TAB_END } + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "a", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "d", { "@ibl.whitespace.char.1" } }, + { "b", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "d", { "@ibl.whitespace.char.1" } }, + { "c", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "d", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles indent with no display width", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "", + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE } + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "e", { "@ibl.whitespace.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles scope", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE } + local scope_active = true + local scope_index = 1 + local scope_end = false + local scope_col_start_single = 2 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "f", { "@ibl.whitespace.char.1", "@ibl.scope.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles tabs", function() + local config = conf.set_config() + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { TAB_START, TAB_FILL, TAB_FILL, TAB_END, TAB_START_SINGLE } + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "a", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "c", { "@ibl.whitespace.char.1" } }, + { "c", { "@ibl.whitespace.char.1" } }, + { "d", { "@ibl.whitespace.char.1" } }, + { "b", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + }) + end) + + it("handles multiple highlight groups", function() + local config = conf.set_config { + whitespace = { highlight = { "Error", "Function", "Label" } }, + indent = { highlight = { "Error", "Function", "Label" } }, + } + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } + local scope_active = false + local scope_index = -1 + local scope_end = false + local scope_col_start_single = 0 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "f", { "@ibl.whitespace.char.2", "@ibl.indent.char.2" } }, + { "e", { "@ibl.whitespace.char.2" } }, + { "f", { "@ibl.whitespace.char.3", "@ibl.indent.char.3" } }, + { "e", { "@ibl.whitespace.char.3" } }, + }) + end) + + it("handles multiple highlight groups with scope", function() + local config = conf.set_config { + whitespace = { highlight = { "Error", "Function", "Label" } }, + indent = { highlight = { "Error", "Function", "Label" } }, + scope = { highlight = { "Error", "Function", "Label" } }, + } + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } + local scope_active = true + local scope_index = 2 + local scope_end = false + local scope_col_start_single = 2 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "f", { "@ibl.whitespace.char.2", "@ibl.scope.char.2" } }, + { "e", { "@ibl.whitespace.char.2" } }, + { "f", { "@ibl.whitespace.char.3", "@ibl.indent.char.3" } }, + { "e", { "@ibl.whitespace.char.3" } }, + }) + end) + + it("handles multiple highlight groups with scope on scope end", function() + local config = conf.set_config { + whitespace = { highlight = { "Error", "Function", "Label" } }, + indent = { highlight = { "Error", "Function", "Label" } }, + scope = { highlight = { "Error", "Function", "Label" } }, + } + highlights.setup() + local char_map = { + [TAB_START] = "a", + [TAB_START_SINGLE] = "b", + [TAB_FILL] = "c", + [TAB_END] = "d", + [SPACE] = "e", + [INDENT] = "f", + } + local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } + local scope_active = true + local scope_index = 2 + local scope_end = true + local scope_col_start_single = 2 + + local virt_text = + vt.get(config, char_map, whitespace_tbl, scope_active, scope_index, scope_end, scope_col_start_single) + + assert.are.same(virt_text, { + { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "f", { "@ibl.whitespace.char.2", "@ibl.scope.char.2", "@ibl.scope.underline.2" } }, + { "e", { "@ibl.whitespace.char.2", "@ibl.scope.underline.2" } }, + { "f", { "@ibl.whitespace.char.3", "@ibl.indent.char.3", "@ibl.scope.underline.2" } }, + { "e", { "@ibl.whitespace.char.3", "@ibl.scope.underline.2" } }, + }) + end) +end) diff --git a/specs/spec.lua b/specs/spec.lua new file mode 100644 index 00000000..1c7635ec --- /dev/null +++ b/specs/spec.lua @@ -0,0 +1,8 @@ +vim.api.nvim_command [[set rtp+=.]] + +vim.opt.swapfile = false +local cwd = vim.fn.getcwd() + +vim.api.nvim_command(string.format([[set rtp+=%s,%s/sepcs]], cwd, cwd)) +vim.api.nvim_command(string.format([[set packpath=%s/vendor]], cwd)) +vim.api.nvim_command [[packloadall]]