diff --git a/CHANGELOG.md b/CHANGELOG.md index 6253739..d9fb981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ code) - Highlights will also take into account `ctermfg/bg` when setting default values. This ensures outline.nvim highlights work if `termguicolors` is not enabled +- A built-in provider for `norg` files that displays headings in the outline is now + provided. This requires `norg` parser to be installed for treesitter ### Fixes diff --git a/README.md b/README.md index 52aed5a..fa17c0d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Table of contents * [Installation](#installation) * [Setup](#setup) * [Configuration](#configuration) +* [Providers](#providers) * [Commands](#commands) * [Default keymaps](#default-keymaps) * [Highlights](#highlights) @@ -59,7 +60,7 @@ Table of contents - Neovim 0.7+ - To use modifiers on [commands](#commands), Neovim 0.8 is required. Everything else works with Neovim 0.7. -- Properly configured Neovim LSP client (otherwise only markdown is supported) +- To use outline.nvim with LSP, a properly configured LSP client is required. ## Installation @@ -333,7 +334,7 @@ Pass a table to the setup call with your configuration options. }, providers = { - priority = { 'lsp', 'coc', 'markdown' }, + priority = { 'lsp', 'coc', 'markdown', 'norg' }, lsp = { -- Lsp client names to ignore blacklist_clients = {}, @@ -448,6 +449,64 @@ The order in which the sources for icons are checked is: A fallback is always used if the previous candidate returned a falsey value. +## Providers + +The current list of tested providers are: +1. LSP (requires a suitable LSP server to be configured for the requested buffer) + - For JSX support, `javascript` parser for treesitter is required +1. Markdown (no external requirements) +1. Norg (requires `norg` parser for treesitter) + +### External providers + +External providers can be appended to the `providers.priority` list. Each +item in the list is appended to `"outline.providers."` to form an import +path, for use as a provider. + +External providers from plugins should define the provider module at +`lua/outline/providers/.lua` with these functions: + +- `supports_buffer(bufnr: integer) -> boolean` + + This function could check buffer filetype, existence of required modules, etc. + +- `get_status() -> string[]` (optional) + + Return a list of lines to be included in `:OutlineStatus` as supplementary + information when this provider is active. + + See an example of this function in the + [LSP](./lua/outline/providers/nvim-lsp.lua) provider. + +- `request_symbols(callback: function, opts: table)` + + - param `callback` is a function that receives a list of symbols and the + `opts` table. + - param `opts` can be passed to `callback` without processing + + Each symbol table in the list of symbols should these fields: + - name: string + - kind: integer + - selectionRange: table with fields `start` and `end`, each have fields + `line` and `character`, each integers + - range: table with fields `start` and `end`, each have fields `line` and + `character`, each integers + - children: list of table of symbols + - detail: (optional) string, shown as `outline_items.show_symbol_details` + +The built-in [markdown](./lua/outline/providers/markdown.lua) provider is a +good example of a very simple outline-provider module which parses raw buffer +lines and uses regex; the built-in [norg](./lua/outline/providers/norg.lua) +provider is an example which uses treesitter. + +All providers should support at least nvim 0.7. You can make use of +`_G._outline_nvim_has` with fields `[8]` and `[9]` equivalent to +`vim.fn.has('nvim-0.8) == 1` and `vim.fn.has('nvim-0.9) == 1` respectively. + +If a higher nvim version is required, it is recommended to check for this +requirement in the `supports_buffer` function. + + ## Commands - **:Outline[!]** (✓ bang ✓ mods) diff --git a/lua/outline/config.lua b/lua/outline/config.lua index 36d6bc6..5c703c8 100644 --- a/lua/outline/config.lua +++ b/lua/outline/config.lua @@ -89,7 +89,7 @@ M.defaults = { up_and_jump = '', }, providers = { - priority = { 'lsp', 'coc', 'markdown' }, + priority = { 'lsp', 'coc', 'markdown', 'norg' }, lsp = { blacklist_clients = {}, }, @@ -211,8 +211,8 @@ end ---@return boolean include function M.should_include_symbol(kind, bufnr) local ft = vim.api.nvim_buf_get_option(bufnr, 'ft') - -- There can only be one kind in markdown as of now - if ft == 'markdown' or kind == nil then + -- There can only be one kind in markdown and norg as of now + if ft == 'markdown' or ft == 'norg' or kind == nil then return true end diff --git a/lua/outline/providers/norg.lua b/lua/outline/providers/norg.lua new file mode 100644 index 0000000..1de1ee8 --- /dev/null +++ b/lua/outline/providers/norg.lua @@ -0,0 +1,135 @@ +local M = { + name = 'norg', + query = [[ + [ + (heading1 (heading1_prefix) + title: (paragraph_segment) @name) + (heading2 (heading2_prefix) + title: (paragraph_segment) @name) + (heading3 (heading3_prefix) + title: (paragraph_segment) @name) + (heading4 (heading4_prefix) + title: (paragraph_segment) @name) + (heading5 (heading5_prefix) + title: (paragraph_segment) @name) + (heading6 (heading6_prefix) + title: (paragraph_segment) @name) + ] + ]], +} + +function M.supports_buffer(bufnr) + if vim.api.nvim_buf_get_option(bufnr, 'ft') ~= 'norg' then + return false + end + + local status, parser = pcall(vim.treesitter.get_parser, bufnr, 'norg') + if not status or not parser then + return false + end + + M.parser = parser + return true +end + +local is_ancestor = vim.treesitter.is_ancestor + +if not _G._outline_nvim_has[8] then + is_ancestor = function(dest, source) + if not (dest and source) then + return false + end + + local current = source + while current ~= nil do + if current == dest then + return true + end + + current = current:parent() + end + + return false + end +end + +local function rec_remove_field(node, field) + node[field] = nil + if node.children then + for _, child in ipairs(node.children) do + rec_remove_field(child, field) + end + end +end + +function M.request_symbols(callback, opts) + if not M.parser then + local status, parser = pcall(vim.treesitter.get_parser, 0, 'norg') + + if not status or not parser then + callback(nil, opts) + return + end + + M.parser = parser + end + + local root = M.parser:parse()[1]:root() + if not root then + callback(nil, opts) + return + end + + local r = { children = {}, tsnode = root, name = 'root' } + local stack = { r } + + local query + if _G._outline_nvim_has[9] then + query = vim.treesitter.query.parse('norg', M.query) + else + ---@diagnostic disable-next-line: deprecated + query = vim.treesitter.query.parse_query('norg', M.query) + end + ---@diagnostic disable-next-line: missing-parameter + for _, captured_node, _ in query:iter_captures(root, 0) do + local row1, col1, row2, col2 = captured_node:range() + local title = vim.api.nvim_buf_get_text(0, row1, col1, row2, col2, {})[1] + local heading_node = captured_node:parent() + row1, col1, row2, col2 = heading_node:range() + + title = title:gsub('^%s+', '') + + local current = { + kind = 15, + name = title, + selectionRange = { + start = { character = col1, line = row1 }, + ['end'] = { character = col2, line = row2 - 1 }, + }, + range = { + start = { character = col1, line = row1 }, + ['end'] = { character = col2, line = row2 - 1 }, + }, + children = {}, + tsnode = heading_node, + } + + while #stack > 0 do + local top = stack[#stack] + if is_ancestor(top.tsnode, heading_node) then + current.parent = top + table.insert(top.children, current) + break + end + table.remove(stack, #stack) + end + + table.insert(stack, current) + end + + rec_remove_field(r, 'tsnode') + + callback(r.children, opts) +end + +return M