diff --git a/doc/indent_blankline.txt b/doc/indent_blankline.txt index 7b21a304..fbf236fa 100644 --- a/doc/indent_blankline.txt +++ b/doc/indent_blankline.txt @@ -201,6 +201,9 @@ config *ibl.config* • {scope} (|ibl.config.scope|) Configures the scope + • {current_indent} (|ibl.config.current_indent|) + Configures the current_indent + • {exclude} (|ibl.config.exclude|) Configures what is excluded from indent-blankline @@ -366,9 +369,11 @@ config.scope *ibl.config.scope* Configures the scope - 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. + The scope is *not* the current indentation level! + See |ibl.config.current_indent| if you want to highlight the current + indentation. + Instead, scope it is the indentation level where variables or functions + are accessible. This depends on the language you are writing. Example: ~ @@ -541,6 +546,78 @@ config.scope.exclude *ibl.config.scope.exclude* } < +config.current_indent *ibl.config.current_indent* + + Configures the current indent + + The current indent *is* just the current indentation level (unlike scope). + + Example: ~ + + In Python, using the same example as for scope, current indent will + highlight the current inner most indentation guide. With scope disabled, + this will look like this: + >python + def foo(); + if True: + ┋ a = "foo █ar" + ┋ # ↳ cursor here + print(a) +< + If we have the same code with scope enabled, it will look like this: + >python + def foo(); + ┋ if True: + ┋ ┋ a = "foo █ar" + ┋ ┋ # ↳ cursor here + ┋ print(a) +< + If you have both scope and current indent enabled, and the two overlap, + |ibl.config.scope.priority| and |ibl.config.current_indent.priority| will + determine which is displayed. + + Fields: ~ + *ibl.config.current_indent.enabled* + • {enabled} (boolean) + Enables or disables current indent + + Default: `false` ~ + + *ibl.config.current_indent.char* + • {char} (string) + Character that gets used to display the current + indentation guide + Each character has to have a display width + of 0 or 1 + + Default: |ibl.config.indent.char| ~ + + *ibl.config.current_indent.highlight* + • {highlight} (string) + Highlight group that gets applied to the current + indent + + Default: |hl-IblCurrentIndent| ~ + + *ibl.config.current_indent.show_start* + • {show_start} (boolean) + Shows an underline on the line above the current indent + + Default: `true` ~ + + *ibl.config.current_indent.show_end* + • {show_end} (boolean) + Shows an underline on the line of the current indent + + Default: `true` ~ + + *ibl.config.current_indent.priority* + • {priority} (number) + Virtual text priority for the current indent + + Default: `1023` ~ + + config.exclude *ibl.config.exclude* Configures what is excluded from indent-blankline @@ -591,7 +668,6 @@ indent.whitespace *ibl.indent.whitespace* • {SPACE} • {INDENT} - hooks *ibl.hooks* Hooks provide a way to extend the functionality of indent-blankline. Either @@ -611,10 +687,12 @@ hooks.type *ibl.hooks.type* Variants: ~ • {ACTIVE} • {SCOPE_ACTIVE} + • {CURRENT_INDENT_ACTIVE} • {SKIP_LINE} • {WHITESPACE} • {VIRTUAL_TEXT} • {SCOPE_HIGHLIGHT} + • {CURRENT_INDENT_HIGHLIGHT} • {CLEAR} • {HIGHLIGHT_SETUP} @@ -653,6 +731,20 @@ hooks.cb.scope_active({bufnr}) *ibl.hooks.cb.scope_active()* (boolean) +hooks.cb.current_indent_active({bufnr}) *ibl.hooks.cb.current_indent_active()* + + Callback function for the |ibl.hooks.type|.CURRENT_INDENT_ACTIVE hook. + + Gets called before refreshing indent-blankline for a buffer. + If the callback returns false, |ibl.config.current_indent| will be disabled. + + Parameters: ~ + • {bufnr} (number) Buffer number + + Return: ~ + (boolean) + + hooks.cb.skip_line({tick}, {bufnr}, {row}, {line}) *ibl.hooks.cb.skip_line()* Callback function for the |ibl.hooks.type|.SKIP_LINE hook. @@ -727,6 +819,26 @@ hooks.cb.scope_highlight({tick}, {bufnr}, {scope}, {scope_index}) (number) + *ibl.hooks.cb.current_indent_highlight()* +hooks.cb.current_indent_highlight({tick}, {bufnr}, {current_indent}, {current_indent_index}) + + Callback function for the |ibl.hooks.type|.CURRENT_INDENT_HIGHLIGHT hook. + + Gets called for once per refresh after the current indent is determined. + The return value overwrites the index of the highlight group + defined in |ibl.config.current_indent.highlight| + + Parameters: ~ + • {tick} (number) auto-incrementing id of the + current refresh + • {bufnr} (number) Buffer number + • {current_indent} (ibl.current_indent) The current scope + • {current_indent_index} (number) Index of the highlight group + + Return: ~ + (number) + + hooks.cb.clear({bufnr}) *ibl.hooks.cb.clear()* Callback function for the |ibl.hooks.type|.CLEAR hook. @@ -800,6 +912,35 @@ hooks.builtin.scope_highlight_from_extmark hooks.type.SCOPE_HIGHLIGHT, hooks.builtin.scope_highlight_from_extmark ) +< + *hooks.builtin.current_indent_highlight_from_extmark* +hooks.builtin.current_indent_highlight_from_extmark + + Gets the highlight group from existing extmark highlights at the end or + beginning of the current indent. + 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. + + Example: ~ + >lua + local highlight = { + "RainbowDelimiterRed", + "RainbowDelimiterYellow", + "RainbowDelimiterBlue", + "RainbowDelimiterOrange", + "RainbowDelimiterGreen", + "RainbowDelimiterViolet", + "RainbowDelimiterCyan", + } + vim.g.rainbow_delimiters = { highlight = highlight } + require "ibl".setup { scope = { highlight = highlight } } + + local hooks = require "ibl.hooks" + hooks.register( + hooks.type.CURRENT_INDENT_HIGHLIGHT, + hooks.builtin.current_indent_highlight_from_extmark + ) < *ibl.hooks.builtin.hide_first_space_indent_level* hooks.builtin.hide_first_space_indent_level @@ -852,6 +993,12 @@ IblScope *hl-IblScope* Default: takes the values from |hl-LineNr| when not defined ~ +IblCurrentIndent *hl-IblCurrentIndent* + + The default highlight group for |ibl.config.current_indent| characters. + + Default: takes the values from |hl-LineNr| when not defined ~ + ============================================================================== 5. COMMANDS *ibl.commands* diff --git a/lua/ibl/config.lua b/lua/ibl/config.lua index 9fa36595..1c71c2c4 100644 --- a/lua/ibl/config.lua +++ b/lua/ibl/config.lua @@ -62,6 +62,17 @@ M.default_config = { }, }, }, + current_indent = { + enabled = false, + char = nil, + show_start = true, + show_end = true, + highlight = "IblCurrentIndent", + priority = 1023, + exclude = { + filetypes = {}, + }, + }, exclude = { filetypes = { "lspinfo", @@ -116,6 +127,7 @@ local validate_config = function(config) indent = { config.indent, "table", true }, whitespace = { config.whitespace, "table", true }, scope = { config.scope, "table", true }, + current_indent = { config.current_indent, "table", true }, exclude = { config.exclude, "table", true }, }, config, "ibl.config") @@ -229,6 +241,43 @@ local validate_config = function(config) end end + if config.current_indent then + utils.validate({ + enabled = { config.current_indent.enabled, "boolean", true }, + char = { config.current_indent.char, { "string", "table" }, true }, + show_start = { config.current_indent.show_start, "boolean", true }, + show_end = { config.current_indent.show_end, "boolean", true }, + highlight = { config.current_indent.highlight, { "string", "table" }, true }, + priority = { config.current_indent.priority, "number", true }, + exclude = { config.current_indent.exclude, "table", true }, + }, config.current_indent, "ibl.config.current_indent") + if config.current_indent.char then + vim.validate { + char = { + config.current_indent.char, + validate_char, + "current_indent.char to have a display width of 0 or 1", + }, + } + end + if type(config.current_indent.highlight) == "table" then + vim.validate { + tab_char = { + config.current_indent.highlight, + function(highlight) + return #highlight > 0 + end, + "current_indent.highlight to be not empty", + }, + } + end + if config.current_indent.exclude then + utils.validate({ + filetypes = { config.current_indent.exclude.filetypes, "table", true }, + }, config.current_indent.exclude, "ibl.config.current_indent.exclude") + end + end + if config.exclude then if config.exclude then utils.validate({ diff --git a/lua/ibl/config.types.lua b/lua/ibl/config.types.lua index eb13694b..c78e36b8 100644 --- a/lua/ibl/config.types.lua +++ b/lua/ibl/config.types.lua @@ -15,6 +15,8 @@ ---@field whitespace ibl.config.whitespace? --- Configures the scope ---@field scope ibl.config.scope? +--- Configures the current_indent +---@field current_indent ibl.config.current_indent? --- Configures what is excluded from indent-blankline ---@field exclude ibl.config.exclude? @@ -109,6 +111,28 @@ --- ---@field node_type table? +---@class ibl.config.current_indent +--- Enables or disables current indent +---@field enabled boolean? +--- Character, or list of characters, that get used to display the current indent 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 current indent +---@field show_start boolean? +--- Shows an underline on the last line of the current indent +---@field show_end boolean? +--- Highlight group, or list of highlight groups, that get applied to the current indent +---@field highlight string|string[]? +--- Virtual text priority for the current indent +---@field priority number? +--- Configures filetypes to be excluded from current indent +---@field exclude ibl.config.current_indent.exclude? + +---@class ibl.config.current_indent.exclude +--- List of filetypes for which current indent is disabled +---@field filetypes string[]? + ---@class ibl.config.exclude --- List of `filetypes` for which indent-blankline is disabled ---@field filetypes string[]? @@ -131,6 +155,8 @@ ---@field whitespace ibl.config.full.whitespace: ibl.config.whitespace --- Configures the scope ---@field scope ibl.config.full.scope: ig.config.scope +--- Configures the current_indent +---@field current_indent ibl.config.full.current_indent --- Configures what is excluded from indent-blankline ---@field exclude ibl.config.full.exclude: ibl.config.exclude @@ -225,6 +251,28 @@ --- ---@field node_type table +---@class ibl.config.full.current_indent +--- Enables or disables current indent +---@field enabled boolean +--- Character, or list of characters, that get used to display the current indent 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 current indent +---@field show_start boolean +--- Shows an underline on the last line of the current indent +---@field show_end boolean +--- Highlight group, or list of highlight groups, that get applied to the current indent +---@field highlight string|string[] +--- Virtual text priority for the current indent +---@field priority number +--- Configures filetypes to be excluded from current indent +---@field exclude ibl.config.full.current_indent.exclude + +---@class ibl.config.full.current_indent.exclude +--- List of filetypes for which current indent is disabled +---@field filetypes string[] + ---@class ibl.config.full.exclude: ibl.config.exclude --- List of `filetypes` for which indent-blankline is disabled ---@field filetypes string[] diff --git a/lua/ibl/highlights.lua b/lua/ibl/highlights.lua index 6bb876f6..1b4b28dc 100644 --- a/lua/ibl/highlights.lua +++ b/lua/ibl/highlights.lua @@ -12,6 +12,8 @@ local M = { whitespace = {}, ---@type ibl.highlight[] scope = {}, + ---@type ibl.highlight[] + current_indent = {}, } ---@param name string @@ -36,6 +38,7 @@ local setup_builtin_hl_groups = function() local ibl_indent_hl_name = "IblIndent" local ibl_whitespace_hl_name = "IblWhitespace" local ibl_scope_hl_name = "IblScope" + local ibl_current_indent_hl_name = "IblCurrentIndent" if not_set(get(ibl_indent_hl_name)) then vim.api.nvim_set_hl(0, ibl_indent_hl_name, whitespace_hl) @@ -46,6 +49,9 @@ local setup_builtin_hl_groups = function() if not_set(get(ibl_scope_hl_name)) then vim.api.nvim_set_hl(0, ibl_scope_hl_name, line_nr_hl) end + if not_set(get(ibl_current_indent_hl_name)) then + vim.api.nvim_set_hl(0, ibl_current_indent_hl_name, line_nr_hl) + end end M.setup = function() @@ -107,6 +113,25 @@ M.setup = function() 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 + + local current_indent_highlights = config.current_indent.highlight + if type(current_indent_highlights) == "string" then + current_indent_highlights = { current_indent_highlights } + end + M.current_indent = {} + for i, current_indent_name in ipairs(current_indent_highlights) do + local char_hl = get(current_indent_name) + if not_set(char_hl) then + error(string.format("No highlight group '%s' found", current_indent_name)) + end + char_hl.nocombine = true + M.current_indent[i] = { + char = string.format("@ibl.current_indent.char.%d", i), + underline = string.format("@ibl.current_indent.underline.%d", i), + } + vim.api.nvim_set_hl(0, M.current_indent[i].char, char_hl) + vim.api.nvim_set_hl(0, M.current_indent[i].underline, { sp = char_hl.fg, underline = true }) + end end return M diff --git a/lua/ibl/hooks.lua b/lua/ibl/hooks.lua index 9e6e9f12..a3c7ddd1 100644 --- a/lua/ibl/hooks.lua +++ b/lua/ibl/hooks.lua @@ -7,10 +7,12 @@ local M = {} M.type = { ACTIVE = "ACTIVE", SCOPE_ACTIVE = "SCOPE_ACTIVE", + CURRENT_INDENT_ACTIVE = "CURRENT_INDENT_ACTIVE", SKIP_LINE = "SKIP_LINE", WHITESPACE = "WHITESPACE", VIRTUAL_TEXT = "VIRTUAL_TEXT", SCOPE_HIGHLIGHT = "SCOPE_HIGHLIGHT", + CURRENT_INDENT_HIGHLIGHT = "CURRENT_INDENT_HIGHLIGHT", CLEAR = "CLEAR", HIGHLIGHT_SETUP = "HIGHLIGHT_SETUP", } @@ -24,10 +26,12 @@ local default_opts = { local hooks = { [M.type.ACTIVE] = {}, [M.type.SCOPE_ACTIVE] = {}, + [M.type.CURRENT_INDENT_ACTIVE] = {}, [M.type.SKIP_LINE] = {}, [M.type.WHITESPACE] = {}, [M.type.VIRTUAL_TEXT] = {}, [M.type.SCOPE_HIGHLIGHT] = {}, + [M.type.CURRENT_INDENT_HIGHLIGHT] = {}, [M.type.CLEAR] = {}, [M.type.HIGHLIGHT_SETUP] = {}, buffer_scoped = {}, @@ -36,10 +40,12 @@ 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.current_indent_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.current_indent_highlight fun(tick: number, bufnr: number, current_indent: ibl.current_indent, current_indent_index: number): number ---@alias ibl.hooks.cb.clear fun(bufnr: number) ---@alias ibl.hooks.cb.highlight_setup fun() @@ -51,10 +57,12 @@ local count = 0 ---@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: 'CURRENT_INDENT_ACTIVE', cb: ibl.hooks.cb.current_indent_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: 'CURRENT_INDENT_HIGHLIGHT', cb: ibl.hooks.cb.current_indent_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) @@ -85,10 +93,12 @@ M.register = function(type, cb, opts) hooks.buffer_scoped[bufnr] = { [M.type.ACTIVE] = {}, [M.type.SCOPE_ACTIVE] = {}, + [M.type.CURRENT_INDENT_ACTIVE] = {}, [M.type.SKIP_LINE] = {}, [M.type.WHITESPACE] = {}, [M.type.VIRTUAL_TEXT] = {}, [M.type.SCOPE_HIGHLIGHT] = {}, + [M.type.CURRENT_INDENT_HIGHLIGHT] = {}, [M.type.CLEAR] = {}, [M.type.HIGHLIGHT_SETUP] = {}, } @@ -119,10 +129,12 @@ M.clear_all = function() hooks = { [M.type.ACTIVE] = {}, [M.type.SCOPE_ACTIVE] = {}, + [M.type.CURRENT_INDENT_ACTIVE] = {}, [M.type.SKIP_LINE] = {}, [M.type.WHITESPACE] = {}, [M.type.VIRTUAL_TEXT] = {}, [M.type.SCOPE_HIGHLIGHT] = {}, + [M.type.CURRENT_INDENT_HIGHLIGHT] = {}, [M.type.CLEAR] = {}, [M.type.HIGHLIGHT_SETUP] = {}, buffer_scoped = {}, @@ -135,10 +147,12 @@ end ---@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: 'CURRENT_INDENT_ACTIVE'): ibl.hooks.cb.current_indent_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: 'CURRENT_INDENT_HIGHLIGHT'): ibl.hooks.cb.current_indent_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) @@ -194,95 +208,25 @@ M.builtin = { ---@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, start_col = scope:start() local end_row, end_col = 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 - local start_pos_scope - local end_pos_scope - 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_pos_scope = vim.inspect_pos(bufnr, end_row, end_col - 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, - }) - start_pos_scope = vim.inspect_pos(bufnr, start_row, start_col, { - extmarks = true, - syntax = false, - treesitter = false, - semantic_tokens = false, - }) - end + return utils.highlight_from_extmark(bufnr, config, start_row, start_col, end_row, end_col, scope_index) + end, - if not end_pos and not start_pos then - return scope_index - end + ---@type ibl.hooks.cb.current_indent_highlight + current_indent_highlight_from_extmark = function(_, bufnr, current_indent, scope_index) + local config = conf.get_config(bufnr) - -- it is most accurate to get correct colors from rainbow-delimiters via - -- the scope, since you can have something like: - -- function() - -- ... - -- end, - -- where the last symbol will give you rainbow-delimiters highlights - -- from the comma (nothing) and the last parenthesis (the wrong color) - for i, hl_group in ipairs(highlight) do - if end_pos_scope then - for _, extmark in ipairs(end_pos_scope.extmarks) do - if extmark.opts.hl_group == hl_group then - return i - end - end - end - if start_pos_scope then - for _, extmark in ipairs(start_pos_scope.extmarks) do - if extmark.opts.hl_group == hl_group then - return i - end - end - end - end - -- For some languages the scope extends before or after the delimiters. Make an attempt to capture them anyway by looking at the first character of the last line, and the last character of the first line. - 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 + return utils.highlight_from_extmark( + bufnr, + config, + current_indent.start_row - 1, + -1, + current_indent.end_row - 1, + -1, + scope_index + ) end, ---@type ibl.hooks.cb.whitespace diff --git a/lua/ibl/indent.lua b/lua/ibl/indent.lua index 0e394719..ee50b9eb 100644 --- a/lua/ibl/indent.lua +++ b/lua/ibl/indent.lua @@ -10,9 +10,13 @@ M.whitespace = { INDENT = 6, } +---@class ibl.indent_state.stack +---@field indent number +---@field row number + ---@class ibl.indent_state ---@field cap boolean ----@field stack table? +---@field stack ibl.indent_state.stack[]? ---@class ibl.indent_options ---@field smart_indent_cap boolean @@ -25,8 +29,9 @@ M.whitespace = { ---@param whitespace string ---@param opts ibl.indent_options ---@param indent_state ibl.indent_state? +---@param row number ---@return ibl.indent.whitespace[], ibl.indent_state -M.get = function(whitespace, opts, indent_state) +M.get = function(whitespace, opts, indent_state, row) if not indent_state then indent_state = { cap = false, stack = {} } end @@ -36,9 +41,9 @@ M.get = function(whitespace, opts, indent_state) local spaces = 0 local tabs = 0 local extra = 0 - local indent_cap = indent_state.stack[#indent_state.stack] or 0 + local indent_cap = indent_state.stack[#indent_state.stack] and indent_state.stack[#indent_state.stack].indent or 0 if indent_state.cap then - indent_cap = indent_state.stack[1] or 0 + indent_cap = indent_state.stack[1] and indent_state.stack[1].indent or 0 indent_state.cap = false end local varts = vim.tbl_map(tonumber, vim.split(vartabstop, ",", { trimempty = true })) @@ -74,13 +79,18 @@ M.get = function(whitespace, opts, indent_state) end else local mod = (spaces + tabs + extra) % shiftwidth - if vim.tbl_contains(indent_state.stack, spaces + tabs) then + if + vim.tbl_contains(indent_state.stack, function(a) + return a.indent == spaces + tabs + end, { predicate = true }) + 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 + table.insert(indent_state.stack, { indent = spaces + tabs, row = row }) else indent_state.cap = true table.insert(whitespace_tbl, M.whitespace.SPACE) @@ -92,10 +102,11 @@ M.get = function(whitespace, opts, indent_state) end end + local indent = spaces + tabs indent_state.stack = vim.tbl_filter(function(a) - return a < spaces + tabs + return a.indent < indent end, indent_state.stack) - table.insert(indent_state.stack, spaces + tabs) + table.insert(indent_state.stack, { indent = indent, row = row }) return whitespace_tbl, indent_state end diff --git a/lua/ibl/init.lua b/lua/ibl/init.lua index e5683a93..18162498 100644 --- a/lua/ibl/init.lua +++ b/lua/ibl/init.lua @@ -169,7 +169,7 @@ M.refresh = function(bufnr) end end - local left_offset, top_offset, win_end, win_height = utils.get_offset(bufnr) + local left_offset, top_offset, win_end, win_height, lnum = utils.get_offset(bufnr) if top_offset > win_end then return end @@ -223,8 +223,18 @@ M.refresh = function(bufnr) } local same_scope = (scope and scope:id()) == (buffer_state.scope and buffer_state.scope:id()) + local current_indent_enabled = utils.is_current_indent_active(bufnr, config) - if not same_scope then + for _, fn in + pairs(hooks.get(bufnr, hooks.type.CURRENT_INDENT_ACTIVE) --[[ @as ibl.hooks.cb.current_indent_active[] ]]) + do + if not fn(bufnr) then + current_indent_enabled = false + break + end + end + + if not same_scope or current_indent_enabled then inlay_hints.clear_buffer(bufnr) end @@ -244,6 +254,19 @@ M.refresh = function(bufnr) end local exact_scope_col_start = scope_col_start + local current_indent_index = -1 + local current_indent_col_start_single = -1 + + ---@class ibl.current_indent + ---@field start_row number + ---@field end_row number + + ---@type ibl.current_indent + local current_indent = { + start_row = -1, + end_row = #lines + offset, + } + ---@type ibl.indent.whitespace[] local last_whitespace_tbl = {} ---@type table @@ -260,11 +283,12 @@ M.refresh = function(bufnr) end end + --- Calc loop --- + local calc_data = {} for i, line in ipairs(lines) do local row = i + offset if line_skipped[i] then - vt.clear_buffer(bufnr, row) - goto continue + goto continue_calc end local whitespace = utils.get_whitespace(line) @@ -273,23 +297,29 @@ M.refresh = function(bufnr) 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 + line_skipped[i] = true + goto continue_calc end end if is_current_buffer and foldclosed > -1 and foldclosed + win_height < row then - vt.clear_buffer(bufnr, row) - goto continue + line_skipped[i] = true + goto continue_calc end ---@type ibl.indent.whitespace[] local whitespace_tbl local blankline = line:len() == 0 - -- #### calculate indent #### if not blankline then - whitespace_tbl, indent_state = indent.get(whitespace, indent_opts, indent_state) + whitespace_tbl, indent_state = indent.get(whitespace, indent_opts, indent_state, row) + if current_indent_enabled and row > lnum and current_indent.end_row > row then + if current_indent_col_start_single == #whitespace_tbl then + current_indent.end_row = row + elseif current_indent_col_start_single > #whitespace_tbl then + current_indent.end_row = row - 1 + end + end elseif empty_line_counter > 0 then empty_line_counter = empty_line_counter - 1 whitespace_tbl = next_whitespace_tbl @@ -306,11 +336,17 @@ M.refresh = function(bufnr) end local j_whitespace = utils.get_whitespace(lines[j]) - whitespace_tbl, indent_state = indent.get(j_whitespace, indent_opts, indent_state) + whitespace_tbl, indent_state = indent.get(j_whitespace, indent_opts, indent_state, row) + + if current_indent_enabled and row > lnum and current_indent.end_row > row then + if current_indent_col_start_single >= #whitespace_tbl then + current_indent.end_row = row - 1 + end + end 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]] + local trail = last_whitespace_tbl[indent_state.stack[#indent_state.stack].indent + 1] + local trail_whitespace = last_whitespace_tbl[indent_state.stack[#indent_state.stack].indent] if trail then table.insert(whitespace_tbl, trail) elseif trail_whitespace then @@ -320,30 +356,14 @@ M.refresh = function(bufnr) table.insert(whitespace_tbl, indent.whitespace.TAB_START) end end + if current_indent.end_row == row - 1 then + current_indent.end_row = j + offset + end end end next_whitespace_tbl = whitespace_tbl end - local scope_active = row >= scope_row_start and row <= scope_row_end - if - scope_active - and scope_col_start_single > -1 - and (whitespace_tbl[scope_col_start_single + 1] or blankline) - and not indent.is_indent(whitespace_tbl[scope_col_start_single + 1]) - then - if indent.is_space_indent(whitespace_tbl[scope_col_start_single + 1]) then - whitespace_tbl[scope_col_start_single + 1] = indent.whitespace.INDENT - else - whitespace_tbl[scope_col_start_single + 1] = indent.whitespace.TAB_START - end - local k = scope_col_start_single - while not whitespace_tbl[k] and k >= 0 do - whitespace_tbl[k] = indent.whitespace.SPACE - k = k - 1 - end - end - -- remove blankline trail if blankline and config.whitespace.remove_blankline_trail then while #whitespace_tbl > 0 do @@ -354,7 +374,7 @@ M.refresh = function(bufnr) end end - -- Fix horizontal scroll + -- fix horizontal scroll local current_left_offset = left_offset while #whitespace_tbl > 0 and current_left_offset > 0 do table.remove(whitespace_tbl, 1) @@ -369,10 +389,67 @@ M.refresh = function(bufnr) last_whitespace_tbl = whitespace_tbl - -- #### make virtual text #### - local scope_start = row == scope_row_start - local scope_end = row == scope_row_end - if scope_start and scope then + --- current indent + if current_indent_enabled and row == lnum then + local current_indent_whitespace_tbl = vim.tbl_extend("keep", {}, whitespace_tbl) + + while #current_indent_whitespace_tbl > 0 do + if indent.is_indent(current_indent_whitespace_tbl[#current_indent_whitespace_tbl]) then + table.remove(current_indent_whitespace_tbl, #current_indent_whitespace_tbl) + break + end + table.remove(current_indent_whitespace_tbl, #current_indent_whitespace_tbl) + end + + current_indent_col_start_single = #current_indent_whitespace_tbl + + for j = indent_state and #indent_state.stack or 0, 1, -1 do + if indent_state.stack[j].indent < current_indent_col_start_single then + break + end + current_indent.start_row = indent_state.stack[j].row + 1 + end + if current_indent.start_row > row then + current_indent.start_row = math.huge + end + + current_indent_index = #vim.tbl_filter(function(w) + return indent.is_indent(w) + end, current_indent_whitespace_tbl) + 1 + end + + do + calc_data[i] = { whitespace, whitespace_tbl } + end + + ::continue_calc:: + end + + for _, fn in + pairs(hooks.get(bufnr, hooks.type.CURRENT_INDENT_HIGHLIGHT) --[[ @as ibl.hooks.cb.current_indent_highlight[] ]]) + do + current_indent_index = fn(buffer_state.tick, bufnr, current_indent, current_indent_index) + end + + --- Draw loop --- + for i, line in ipairs(lines) do + local row = i + offset + if line_skipped[i] then + vt.clear_buffer(bufnr, row) + goto continue_draw + end + local whitespace, whitespace_tbl = unpack(calc_data[i]) + + local is_current_indent_active = row >= current_indent.start_row + and row <= (current_indent.end_row or #lines + offset) + local is_current_indent_start = row + 1 == (current_indent.start_row or -1) + local is_current_indent_end = row == (current_indent.end_row or -1) + + local is_scope_active = row >= scope_row_start and row <= scope_row_end + local is_scope_start = row == scope_row_start + local is_scope_end = row == scope_row_end + + if is_scope_start and scope then scope_col_start = #whitespace scope_col_start_single = #whitespace_tbl scope_index = #vim.tbl_filter(function(w) @@ -385,10 +462,22 @@ M.refresh = function(bufnr) end end + local blankline = line:len() == 0 local whitespace_only = not blankline and line == whitespace 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) + local virt_text, scope_hl, current_indent_hl = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) -- #### set virtual text #### vt.clear_buffer(bufnr, row) @@ -402,8 +491,18 @@ M.refresh = function(bufnr) scope_show_end_cond = #whitespace_tbl >= scope_col_start_single end - -- Scope start - if config.scope.show_start and scope_start then + -- scope start + if + config.scope.enabled + and config.scope.show_start + and is_scope_start + and not ( + current_indent_enabled + and config.current_indent.show_start + and is_current_indent_start + and config.current_indent.priority > config.scope.priority + ) + then vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, scope_col_start_draw, { end_col = #line, hl_group = scope_hl.underline, @@ -413,8 +512,20 @@ M.refresh = function(bufnr) 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 scope_show_end_cond then + -- scope end + if + config.scope.enabled + and config.scope.show_end + and is_scope_end + and scope_show_end_cond + and not ( + current_indent_enabled + and config.current_indent.show_end + and is_current_indent_end + and #whitespace_tbl > current_indent_col_start_single + and config.current_indent.priority > config.scope.priority + ) + then vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, scope_col_start, { end_col = scope_col_end, hl_group = scope_hl.underline, @@ -424,6 +535,50 @@ M.refresh = function(bufnr) inlay_hints.set(bufnr, row - 1, #whitespace, scope_hl.underline, scope_hl.underline) end + -- current indent start + if + current_indent_enabled + and config.current_indent.show_start + and is_current_indent_start + and not ( + config.scope.enabled + and config.scope.show_start + and is_scope_start + and config.scope.priority >= config.current_indent.priority + ) + then + vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, #whitespace, { + end_col = #line, + hl_group = current_indent_hl.underline, + priority = config.current_indent.priority, + strict = false, + }) + inlay_hints.set(bufnr, row - 1, #whitespace, current_indent_hl.underline, current_indent_hl.underline) + end + + -- current indent end + if + current_indent_enabled + and config.current_indent.show_end + and is_current_indent_end + and #whitespace_tbl > current_indent_col_start_single + and not ( + config.scope.enabled + and config.scope.show_end + and is_scope_end + and scope_show_end_cond + and config.scope.priority >= config.current_indent.priority + ) + then + vim.api.nvim_buf_set_extmark(bufnr, namespace, row - 1, current_indent_col_start_single, { + end_col = #line, + hl_group = current_indent_hl.underline, + priority = config.current_indent.priority, + strict = false, + }) + inlay_hints.set(bufnr, row - 1, #whitespace, current_indent_hl.underline, current_indent_hl.underline) + end + for _, fn in pairs(hooks.get(bufnr, hooks.type.VIRTUAL_TEXT) --[[ @as ibl.hooks.cb.virtual_text[] ]]) do @@ -441,7 +596,7 @@ M.refresh = function(bufnr) }) end - ::continue:: + ::continue_draw:: end end diff --git a/lua/ibl/utils.lua b/lua/ibl/utils.lua index addbfa34..d9cd7312 100644 --- a/lua/ibl/utils.lua +++ b/lua/ibl/utils.lua @@ -168,7 +168,7 @@ M.get_offset = function(bufnr) else local win_list = vim.fn.win_findbuf(bufnr) if not win_list or not win_list[1] then - return 0, 0, 0, 0 + return 0, 0, 0, 0, 0 end win = win_list[1] win_view = vim.api.nvim_win_call(win, vim.fn.winsaveview) @@ -183,7 +183,7 @@ M.get_offset = function(bufnr) win_end = win_view.lnum + win_height end - return win_view.leftcol or 0, win_view.topline or 0, win_end, win_height + return win_view.leftcol or 0, win_view.topline or 0, win_end, win_height, win_view.lnum or 0 end ---@param bufnr number @@ -203,6 +203,21 @@ M.is_buffer_active = function(bufnr, config) return true end +---@param bufnr number +---@param config ibl.config +M.is_current_indent_active = function(bufnr, config) + if not config.current_indent.enabled then + return false + end + for _, filetype in ipairs(M.get_filetypes(bufnr)) do + if vim.tbl_contains(config.current_indent.exclude.filetypes, filetype) then + return false + end + end + + return true +end + ---@param bufnr number ---@return number M.get_bufnr = function(bufnr) @@ -231,4 +246,94 @@ M.tbl_get_index = function(list, i) return list[((i - 1) % #list) + 1] end +M.highlight_from_extmark = function(bufnr, config, start_row, start_col, end_row, end_col, fallback_index) + local highlight = config.scope.highlight + + if type(highlight) ~= "table" or start_row == math.huge then + return fallback_index + 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 + local start_pos_col + local end_pos_col + + if end_line[1] then + end_pos = vim.inspect_pos(bufnr, end_row, end_line[1]:find "%S" - 1, { + extmarks = true, + syntax = false, + treesitter = false, + semantic_tokens = false, + }) + end_pos_col = vim.inspect_pos(bufnr, end_row, end_col - 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, + }) + start_pos_col = vim.inspect_pos(bufnr, start_row, start_col, { + extmarks = true, + syntax = false, + treesitter = false, + semantic_tokens = false, + }) + end + + if not end_pos and not start_pos then + return fallback_index + end + + -- it is most accurate to get correct colors from rainbow-delimiters via + -- the scope, since you can have something like: + -- function() + -- ... + -- end, + -- where the last symbol will give you rainbow-delimiters highlights + -- from the comma (nothing) and the last parenthesis (the wrong color) + for i, hl_group in ipairs(highlight) do + if end_pos_col then + for _, extmark in ipairs(end_pos_col.extmarks) do + if extmark.opts.hl_group == hl_group then + return i + end + end + end + if start_pos_col then + for _, extmark in ipairs(start_pos_col.extmarks) do + if extmark.opts.hl_group == hl_group then + return i + end + end + end + end + -- For some languages the scope extends before or after the delimiters. Make an attempt to capture them anyway by looking at the first character of the last line, and the last character of the first line. + 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 fallback_index +end + return M diff --git a/lua/ibl/virt_text.lua b/lua/ibl/virt_text.lua index d59c8d9d..19a851be 100644 --- a/lua/ibl/virt_text.lua +++ b/lua/ibl/virt_text.lua @@ -57,51 +57,116 @@ end ---@param config ibl.config ---@param char_map ibl.char_map ---@param whitespace_tbl ibl.indent.whitespace[] ----@param scope_active boolean +---@param is_current_indent_active boolean +---@param current_indent_index number +---@param is_current_indent_end boolean +---@param current_indent_col_start_single number +---@param is_scope_active boolean ---@param scope_index number ----@param scope_end boolean +---@param is_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) +---@return ibl.virtual_text, ibl.highlight, ibl.highlight +M.get = function( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single +) local scope_hl = utils.tbl_get_index(highlights.scope, scope_index) + local current_indent_hl = utils.tbl_get_index(highlights.current_indent, current_indent_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) + local sa = is_scope_active + local cia = is_current_indent_active + local indent_char = get_char(char_map[ws], indent_index) + local char = indent_char 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 + cia = 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 + local set_current_indent = function() + if + config.current_indent.show_end + and cia + and is_current_indent_end + and i - 1 > current_indent_col_start_single + then + current_indent_hl = utils.tbl_get_index(highlights.current_indent, current_indent_index) + underline_hl = current_indent_hl.underline + end - if sa and i - 1 == scope_col_start_single then - indent_hl = scope_hl.char + if cia and i - 1 == current_indent_col_start_single then + indent_hl = current_indent_hl.char + + if config.current_indent.char then + local current_indent_char = get_char(config.current_indent.char, current_indent_index) + if vim.fn.strdisplaywidth(current_indent_char) == 1 then + char = current_indent_char + else + char = indent_char + end + else + char = indent_char + end - 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 + if config.current_indent.show_end and is_current_indent_end then + underline_hl = current_indent_hl.underline end end + end - if config.scope.show_end and scope_end then + local set_scope = function() + if config.scope.show_end and sa and is_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 + else + char = indent_char + end + else + char = indent_char + end + + if config.scope.show_end and is_scope_end then + underline_hl = scope_hl.underline + end + end + end + + if config.scope.priority >= config.current_indent.priority then + set_current_indent() + set_scope() + else + set_scope() + set_current_indent() end table.insert(virt_text, { @@ -112,7 +177,7 @@ M.get = function(config, char_map, whitespace_tbl, scope_active, scope_index, sc }) end - return virt_text, scope_hl + return virt_text, scope_hl, current_indent_hl end return M diff --git a/specs/features/indent_spec.lua b/specs/features/indent_spec.lua index 6c451987..c443d6a3 100644 --- a/specs/features/indent_spec.lua +++ b/specs/features/indent_spec.lua @@ -19,40 +19,40 @@ describe("indent", function() end) it("no whitespace", function() - local whitespace_tbl, _ = indent.get("", opts) + local whitespace_tbl, _ = indent.get("", opts, nil, 1) assert.are.same(whitespace_tbl, {}) end) it("normal space indentation", function() - local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) assert.are.same(whitespace_tbl, { INDENT, SPACE }) end) it("normal tab", function() - local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) 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 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) 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 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) 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 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) assert.are.same( whitespace_tbl, @@ -61,33 +61,48 @@ describe("indent", function() end) it("mix of tabs and spaces", function() - local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { 0 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) 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 } }) + local whitespace_tbl, _ = indent.get(" ", opts, { cap = false, stack = { { indent = 0, row = 1 } } }, 2) 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 } }) + local whitespace_tbl, _ = indent.get( + " ", + opts, + { cap = false, stack = { { indent = 0, row = 1 }, { indent = 4, row = 2 } } }, + 3 + ) 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 } }) + local whitespace_tbl, _ = indent.get( + " ", + opts, + { cap = true, stack = { { indent = 0, row = 1 }, { indent = 4, row = 2 } } }, + 2 + ) 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 } }) + local whitespace_tbl, _ = indent.get( + " ", + opts, + { cap = false, stack = { { indent = 0, row = 1 }, { indent = 4, row = 2 } } }, + 3 + ) assert.are.same(whitespace_tbl, { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE, INDENT, SPACE }) end) diff --git a/specs/features/virt_text_spec.lua b/specs/features/virt_text_spec.lua index 2e39c927..b6848caa 100644 --- a/specs/features/virt_text_spec.lua +++ b/specs/features/virt_text_spec.lua @@ -278,13 +278,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = {} - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, {}) end) @@ -301,13 +316,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE } - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -329,13 +359,28 @@ describe("virt_text", function() [INDENT] = { "a", "b", "c" }, } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "a", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -359,13 +404,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { TAB_START, TAB_END, TAB_START, TAB_END, TAB_START, TAB_END } - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "a", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -389,13 +449,28 @@ describe("virt_text", function() [INDENT] = "", } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE } - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "e", { "@ibl.whitespace.char.1" } }, @@ -417,13 +492,157 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE } - local scope_active = true + local is_scope_active = true local scope_index = 1 - local scope_end = false + local is_scope_end = false local scope_col_start_single = 2 + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) - 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 current indent", 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 is_scope_active = false + local scope_index = -1 + local is_scope_end = false + local scope_col_start_single = -1 + local is_current_indent_active = true + local current_indent_index = 1 + local is_current_indent_end = false + local current_indent_col_start_single = 2 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_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.current_indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles scope and current indent", 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 is_scope_active = true + local scope_index = 1 + local is_scope_end = false + local scope_col_start_single = 0 + local is_current_indent_active = true + local current_indent_index = 1 + local is_current_indent_end = false + local current_indent_col_start_single = 2 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) + + assert.are.same(virt_text, { + { "f", { "@ibl.whitespace.char.1", "@ibl.scope.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + { "f", { "@ibl.whitespace.char.1", "@ibl.current_indent.char.1" } }, + { "e", { "@ibl.whitespace.char.1" } }, + }) + end) + + it("handles scope and current indent priority", function() + local config = conf.set_config { current_indent = { char = "q" } } + 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 is_scope_active = true + local scope_index = 1 + local is_scope_end = false + local scope_col_start_single = 2 + local is_current_indent_active = true + local current_indent_index = 1 + local is_current_indent_end = false + local current_indent_col_start_single = 2 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -445,13 +664,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { TAB_START, TAB_FILL, TAB_FILL, TAB_END, TAB_START_SINGLE } - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "a", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -477,13 +711,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } - local scope_active = false + local is_scope_active = false local scope_index = -1 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -511,13 +760,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } - local scope_active = true + local is_scope_active = true local scope_index = 2 - local scope_end = false + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -529,6 +793,55 @@ describe("virt_text", function() }) end) + it("handles multiple highlight groups with curent indent", function() + local config = conf.set_config { + whitespace = { highlight = { "Error", "Function", "Label" } }, + indent = { highlight = { "Error", "Function", "Label" } }, + current_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 is_scope_active = false + local scope_index = -1 + local is_scope_end = false + local scope_col_start_single = -1 + local is_current_indent_active = true + local current_indent_index = 2 + local is_current_indent_end = false + local current_indent_col_start_single = 2 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_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.current_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 on scope end", function() local config = conf.set_config { whitespace = { highlight = { "Error", "Function", "Label" } }, @@ -545,13 +858,28 @@ describe("virt_text", function() [INDENT] = "f", } local whitespace_tbl = { INDENT, SPACE, INDENT, SPACE, INDENT, SPACE } - local scope_active = true + local is_scope_active = true local scope_index = 2 - local scope_end = true + local is_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) + local is_current_indent_active = false + local current_indent_index = -1 + local is_current_indent_end = false + local current_indent_col_start_single = -1 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_scope_end, + scope_col_start_single + ) assert.are.same(virt_text, { { "f", { "@ibl.whitespace.char.1", "@ibl.indent.char.1" } }, @@ -562,4 +890,53 @@ describe("virt_text", function() { "e", { "@ibl.whitespace.char.3", "@ibl.scope.underline.2" } }, }) end) + + it("handles multiple highlight groups with current_indent on current_indent end", function() + local config = conf.set_config { + whitespace = { highlight = { "Error", "Function", "Label" } }, + indent = { highlight = { "Error", "Function", "Label" } }, + current_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 is_scope_active = false + local scope_index = -1 + local is_scope_end = false + local scope_col_start_single = -1 + local is_current_indent_active = true + local current_indent_index = 2 + local is_current_indent_end = true + local current_indent_col_start_single = 2 + + local virt_text = vt.get( + config, + char_map, + whitespace_tbl, + is_current_indent_active, + current_indent_index, + is_current_indent_end, + current_indent_col_start_single, + is_scope_active, + scope_index, + is_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.current_indent.char.2", "@ibl.current_indent.underline.2" } }, + { "e", { "@ibl.whitespace.char.2", "@ibl.current_indent.underline.2" } }, + { "f", { "@ibl.whitespace.char.3", "@ibl.indent.char.3", "@ibl.current_indent.underline.2" } }, + { "e", { "@ibl.whitespace.char.3", "@ibl.current_indent.underline.2" } }, + }) + end) end)