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()
```
-
+
-#### 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()
```
-
+
-#### 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()
```
-
+
-#### 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 } }
```
-
+
-#### 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 },
}
```
-
+
-`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)
+
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]]