Virt-Indent.nvim/lua/vindent/indent.lua

179 lines
5.9 KiB
Lua
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---@class VirtualIndent
---@field private _ns_id number extmarks namespace id
---@field private _bufnr integer Buffer VirtualIndent is attached to
---@field private _attached boolean Whether or not VirtualIndent is attached for its buffer
---@field private _bufnrs table<integer, VirtualIndent> Buffers with VirtualIndent attached
---@field private _watcher_running boolean Whether or not VirtualIndent is reacting to `vim.b.org_indent_mode`
---@field private _extmark_ids table<integer, integer[]> Mapping of line number to active extmarks
---@field private _tree_utils table<string, function> Treesitter utilities for the given filetype
---@field private _fallback_pattern string Pattern to search for if treesitter parser fails
local VirtualIndent = {
_ns_id = vim.api.nvim_create_namespace("VirtIndent"),
_bufnrs = {},
}
VirtualIndent.__index = VirtualIndent
--- Creates a new instance of VirtualIndent for a given buffer or returns the existing instance if
--- one exists
---@param bufnr? integer Buffer to use for VirtualIndent when attached
---@return VirtualIndent?
function VirtualIndent:new(bufnr)
-- TODO: Improve organization of this, ideally should be a separate module that returns these.
local ft_settings = {
markdown = {
utils = require("vindent.treesitter.markdown"),
fallback_pattern = "^%#+",
},
org = {
utils = require("vindent.treesitter.org"),
fallback_pattern = "^%*+",
},
}
local filetype = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) or ""
local ft_setting = ft_settings[filetype]
if not ft_setting then
return
end
bufnr = bufnr or vim.api.nvim_get_current_buf()
if self._bufnrs[bufnr] then
return self._bufnrs[bufnr]
end
local this = setmetatable({
_bufnr = bufnr,
_watcher_running = false,
_extmark_ids = {},
_attached = false,
_fallback_pattern = ft_setting.fallback_pattern,
_tree_utils = ft_setting.utils,
}, self)
self._bufnrs[bufnr] = this
return this
end
function VirtualIndent:_delete_old_extmarks(start_line, end_line)
for line = start_line, end_line do
local line_extmark_ids = self._extmark_ids[line] or {}
for idx, extmark_id in ipairs(line_extmark_ids) do
vim.api.nvim_buf_del_extmark(self._bufnr, self._ns_id, extmark_id)
line_extmark_ids[idx] = nil
end
end
end
function VirtualIndent:_get_indent_size(line, ts_has_errors, ts_fallback_pat)
-- If tree has errors, we can't rely on treesitter to get the correct indentation
-- Fallback to searching closest headline by checking each previous line
if ts_has_errors then
local linenr = line
while linenr > 0 do
local _, level = vim.fn.getline(linenr):find(ts_fallback_pat)
if level then
-- If the current line is a headline we should return no virtual indentation, otherwise
-- return virtual indentation
return (linenr == line and 0 or level + 1)
end
linenr = linenr - 1
end
end
local headline = self._tree_utils.closest_headline_node({ line + 1, 1 })
if headline then
local headline_line = headline:start()
if headline_line ~= line then
return self._tree_utils.headline_level(headline) + 1
end
end
return 0
end
---@param start_line number start line number to set the indentation, 0-based inclusive
---@param end_line number end line number to set the indentation, 0-based inclusive
---@param ignore_ts? boolean whether or not to skip the treesitter start & end lookup
function VirtualIndent:set_indent(start_line, end_line, ignore_ts)
ignore_ts = ignore_ts or false
local headline = self._tree_utils.closest_headline_node({ start_line + 1, 1 })
if headline and not ignore_ts then
local parent = headline:parent()
if parent then
start_line = math.min(parent:start(), start_line)
end_line = math.max(parent:end_(), end_line)
end
end
if start_line > 0 then
start_line = start_line - 1
end
local node_at_cursor = vim.treesitter.get_node()
local ts_has_errors = false
if node_at_cursor then
ts_has_errors = node_at_cursor:tree():root():has_error()
end
self:_delete_old_extmarks(start_line, end_line)
for line = start_line, end_line do
local indent = self:_get_indent_size(line, ts_has_errors, self._fallback_pattern)
if indent > 0 then
-- NOTE: `ephemeral = true` is not implemented for `inline` virt_text_pos :(
local new_extmark_id = vim.api.nvim_buf_set_extmark(self._bufnr, self._ns_id, line, 0, {
-- HACK: The 'space' character below is not a space, it is actually a "Braille Pattern Blank"
-- character, U+2800. This avoids issues with how `indentexpr` is calculated by not using
-- spaces (which the `indentexpr` is looking for).
virt_text = { { string.rep("", indent), "VirtIndent" } },
virt_text_pos = "inline",
right_gravity = false,
priority = 110,
})
if not self._extmark_ids[line] then
self._extmark_ids[line] = { new_extmark_id }
else
table.insert(self._extmark_ids[line], new_extmark_id)
end
end
end
end
--- Enables virtual indentation in registered buffer
function VirtualIndent:attach()
if self._attached then
return
end
self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr) - 1, true)
vim.api.nvim_buf_attach(self._bufnr, false, {
on_lines = function(_, _, _, start_line, _, end_line)
if not self._attached then
return true
end
vim.schedule(function()
self:set_indent(start_line, end_line)
end)
end,
on_reload = function()
self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr) - 1, true)
end,
on_detach = function(_, bufnr)
self:detach()
self._bufnrs[bufnr] = nil
end,
})
self._attached = true
end
function VirtualIndent:detach()
if not self._attached then
return
end
self:_delete_old_extmarks(0, vim.api.nvim_buf_line_count(self._bufnr) - 1)
self._attached = false
end
return VirtualIndent