From e2d177b480ad315b6e43689d0ae8e46d6c39d673 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Mon, 15 Apr 2024 06:31:43 -0500 Subject: [PATCH] feat: initial commit --- .editorconfig | 3 + .gitignore | 43 +++++++ LICENSE | 21 ++++ Makefile | 8 ++ README.md | 34 ++++++ assets/after.png | Bin 0 -> 19498 bytes assets/before.png | Bin 0 -> 19484 bytes ftplugin/markdown.lua | 1 + ftplugin/org.lua | 1 + lua/vindent/indent.lua | 178 ++++++++++++++++++++++++++++ lua/vindent/init.lua | 23 ++++ lua/vindent/treesitter/markdown.lua | 53 +++++++++ lua/vindent/treesitter/org.lua | 46 +++++++ stylua.toml | 4 + tests/minimal_init.lua | 92 ++++++++++++++ tests/test.lua | 14 +++ 16 files changed, 521 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/after.png create mode 100644 assets/before.png create mode 100644 ftplugin/markdown.lua create mode 100644 ftplugin/org.lua create mode 100644 lua/vindent/indent.lua create mode 100644 lua/vindent/init.lua create mode 100644 lua/vindent/treesitter/markdown.lua create mode 100644 lua/vindent/treesitter/org.lua create mode 100644 stylua.toml create mode 100644 tests/minimal_init.lua create mode 100644 tests/test.lua diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9a6cc75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.lua] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..415c879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Directory containing testing dependencies downloaded during test run +.deps/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..49802b7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT Licence + +Copyright (c) 2024 Price Hiller + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ef7ccc6 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +clean: + nvim --headless --clean -n -c "lua vim.fn.delete('./tests/.deps', 'rf')" +q +test: + nvim --headless --clean -u tests/test.lua "$(FILE)" +lint: + stylua --check lua/ tests/ +format: + stylua lua/ tests/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..19946a5 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Virt-Indent.nvim + +Add virtual indentation to align content under headlines to their headings. + +## Markdown Showcase + +_Before:_ +![Before](./assets/before.png) + +_After:_ +![After](./assets/after.png) + +## Quick Start + +### Requirements + +- Neovim 0.10.0 or later + +### Installation + +- [**lazy.nvim**](https://github.com/folke/lazy.nvim) + ```lua + { + "PriceHiller/Virt-Indent.nvim", + ft = { "org", "markdown" }, + } + ``` + +## Credits + +This plugin is a module extracted from [nvim-orgmode](https://github.com/nvim-orgmode/orgmode/), specifically its [`virtual-indent`](https://github.com/nvim-orgmode/orgmode/blob/master/lua/orgmode/ui/virtual_indent.lua) module and adds additional support for more filetypes beyond org files. + +- [@danilshvalov](https://github.com/danilshvalov), the original creator of some of this code. I ultimately took the existing code he wrote and carried a PR with my own additions to completion and integration into nvim-orgmode. +- [@kristijanhusak](https://github.com/kristijanhusak), the creator/maintainer of nvim-orgmode. He has done an amazing amount of work on that plugin and much of what was done in this plugin would not have come into being without him. diff --git a/assets/after.png b/assets/after.png new file mode 100644 index 0000000000000000000000000000000000000000..67a464505dadffe2a0fbbf2844e388c23b2f28c2 GIT binary patch literal 19498 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�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 Rjn)7F002ovPDHLkV1n;wgUtW{ literal 0 HcmV?d00001 diff --git a/assets/before.png b/assets/before.png new file mode 100644 index 0000000000000000000000000000000000000000..df42b380c4d3182effcf85fa0e05761ae9276157 GIT binary patch literal 19484 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 KMNUMnLSTXj Buffers with VirtualIndent attached +---@field private _watcher_running boolean Whether or not VirtualIndent is reacting to `vim.b.org_indent_mode` +---@field private _tree_utils table 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, + _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) + local ok, old_extmarks = pcall( + vim.api.nvim_buf_get_extmarks, + self._bufnr, + self._ns_id, + { start_line, 0 }, + { end_line, 0 }, + { type = "virt_text" } + ) + if not ok then + old_extmarks = {} + end + for _, ext in ipairs(old_extmarks) do + vim.api.nvim_buf_del_extmark(self._bufnr, self._ns_id, ext[1]) + 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 :( + pcall(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, + }) + 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 diff --git a/lua/vindent/init.lua b/lua/vindent/init.lua new file mode 100644 index 0000000..107452b --- /dev/null +++ b/lua/vindent/init.lua @@ -0,0 +1,23 @@ +local M = {} + +function M.ftplugin_setup() + if vim.b.did_ftplugin then + return + end + vim.b.did_ftplugin = true + + vim.b.vindent_enabled = true + local vindent = require("vindent.indent"):new() + if vindent then + vindent:attach() + end +end + +---Gets the current VirtualIndent instance +---@param buf? number Buffer number or 0 for current buffer +---@return VirtualIndent? The VirtualIndent instance if it exists +function M.get_buf_vindent(buf) + return require("vindent.indent"):new(buf) +end + +return M diff --git a/lua/vindent/treesitter/markdown.lua b/lua/vindent/treesitter/markdown.lua new file mode 100644 index 0000000..c384cc7 --- /dev/null +++ b/lua/vindent/treesitter/markdown.lua @@ -0,0 +1,53 @@ +local M = {} + +function M.find_headline(node) + if node:type() == "atx_heading" then + return node + end + + if node:type() == "section" then + -- The headline is always the first child of a section + local child = node:child "atx_heading" + if child then + return child + end + end + + if node:parent() then + return M.find_headline(node:parent()) + end + + return nil +end + +function M.headline_level(headline) + local heading_content = headline:field "heading_content" + if not heading_content or #heading_content == 0 then + return 0 + end + local _, level = heading_content[1]:start() + return level - 1 +end + +function M.get_node_at_cursor(cursor) + if not cursor then + return vim.treesitter.get_node() + end + + return vim.treesitter.get_node { + bufnr = 0, + pos = { cursor[1] - 1, cursor[2] }, + } +end + +function M.closest_headline_node(cursor) + local node = M.get_node_at_cursor(cursor) + + if not node then + return nil + end + + return M.find_headline(node) +end + +return M diff --git a/lua/vindent/treesitter/org.lua b/lua/vindent/treesitter/org.lua new file mode 100644 index 0000000..b5c6fa5 --- /dev/null +++ b/lua/vindent/treesitter/org.lua @@ -0,0 +1,46 @@ +local M = {} + +function M.find_headline(node) + if node:type() == "headline" then + return node + end + + if node:type() == "section" then + -- The headline is always the first child of a section + return node:child("headline")[1] + end + + if node:parent() then + return M.find_headline(node:parent()) + end + + return nil +end + +function M.get_node_at_cursor(cursor) + if not cursor then + return vim.treesitter.get_node() + end + + return vim.treesitter.get_node { + bufnr = 0, + pos = { cursor[1] - 1, cursor[2] }, + } +end + +function M.headline_level(headline) + local _, level = headline:field("stars")[1]:end_() + return level + 1 +end + +function M.closest_headline_node(cursor) + local node = M.get_node_at_cursor(cursor) + + if not node then + return nil + end + + return M.find_headline(node) +end + +return M diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..5b6fff8 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,4 @@ +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +no_call_parentheses = true diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua new file mode 100644 index 0000000..157c55f --- /dev/null +++ b/tests/minimal_init.lua @@ -0,0 +1,92 @@ +local M = {} + +---@class OrgMinPlugin A plugin to download and register on the package path +---@alias OrgPluginName string The plugin name, will be used as part of the git clone destination +---@alias OrgPluginUrl string The git url at which a plugin is located, can be a path. See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols for details +---@alias OrgMinPlugins table + +-- Gets the current directory of this file +local base_root_path = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h") +---Gets the root directory of the minimal init and if path is specified appends the given path to the root allowing for +---subdirectories within the current cwd +---@param path string? The additional path to append to the root, not required +---@return string root The root path suffixed with the path provided or an empty suffix if none was given +function M.root(path) + return base_root_path .. "/.deps/" .. (path or "") +end + +---Downloads a plugin from a given url and registers it on the 'runtimepath' +---@param plugin_name OrgPluginName +---@param plugin_url OrgPluginUrl +function M.load_plugin(plugin_name, plugin_url) + local package_root = M.root "plugins/" + local install_destination = package_root .. plugin_name + vim.opt.runtimepath:append(install_destination) + + if not vim.loop.fs_stat(package_root) then + vim.fn.mkdir(package_root, "p") + end + + -- If the plugin install path already exists, we don't need to clone it again. + if not vim.loop.fs_stat(install_destination) then + print(string.format('>> Downloading plugin "%s" to "%s"', plugin_name, install_destination)) + vim.fn.system { + "git", + "clone", + "--depth=1", + plugin_url, + install_destination, + } + if vim.v.shell_error > 0 then + error( + string.format('>> Failed to clone plugin: "%s" to "%s"!', plugin_name, install_destination), + vim.log.levels.ERROR + ) + end + end +end + +---Do the initial setup. Downloads plugins, ensures the minimal init does not pollute the filesystem by keeping +---everything self contained to the CWD of the minimal init file. Run prior to running tests, reproducing issues, etc. +---@param plugins? OrgMinPlugins +function M.setup(plugins) + vim.opt.packpath = {} -- Empty the package path so we use only the plugins specified + vim.opt.runtimepath:append(M.root ".min") -- Ensure the runtime detects the root min dir + + -- Install required plugins + if plugins ~= nil then + for plugin_name, plugin_url in pairs(plugins) do + M.load_plugin(plugin_name, plugin_url) + end + end + + vim.env.XDG_CONFIG_HOME = M.root "xdg/config" + vim.env.XDG_DATA_HOME = M.root "xdg/data" + vim.env.XDG_STATE_HOME = M.root "xdg/state" + vim.env.XDG_CACHE_HOME = M.root "xdg/cache" + + local std_paths = { + "cache", + "data", + "config", + } + + for _, std_path in pairs(std_paths) do + vim.fn.mkdir(vim.fn.stdpath(std_path), "p") + end + + -- NOTE: Cleanup the xdg cache on exit so new runs of the minimal init doesn't share any previous state, e.g. shada + vim.api.nvim_create_autocmd("VimLeave", { + callback = function() + vim.fn.delete(M.root "xdg", "rf") + end, + }) +end + +M.setup { + plenary = "https://github.com/nvim-lua/plenary.nvim.git", + treesitter = "https://github.com/nvim-treesitter/nvim-treesitter", +} + +-- WARN: Do all plugin setup, test runs, reproductions, etc. AFTER calling setup with a list of plugins! +-- Basically, do all that stuff AFTER this line. diff --git a/tests/test.lua b/tests/test.lua new file mode 100644 index 0000000..9a51689 --- /dev/null +++ b/tests/test.lua @@ -0,0 +1,14 @@ +require "tests.minimal_init" +---@type string +local test_file = vim.v.argv[#vim.v.argv] +if test_file == "" or not test_file:find("tests/plenary/", nil, true) then + test_file = "tests/tests" + print("Running all tests at " .. test_file) +else + print("Individual Test File/Directory provided: " .. test_file) +end + +require("plenary.test_harness").test_directory(test_file, { + minimal_init = "tests/minimal_init.lua", + sequential = true, +})