From d35187ef37284a74a4f57041c6991b850dc71152 Mon Sep 17 00:00:00 2001 From: hedy Date: Wed, 22 Nov 2023 22:16:19 +0800 Subject: [PATCH] feat: Norg provider and support of external providers - Closes #3 - Ref: simrat39/symbols-outline.nvim#190 Norg contains indents and different types of verbatim tags, I was rather lazy to read the spec properly and parse norg using regex line-by-line like markdown, so used treesitter instead. The only requirement is the `norg` parser for treesitter to be installed. Tested on nvim 0.7.2. This should lead the way for supporting vimdoc files in a similar manner. Documentation for how external providers could look like as of now has been added. In the future we could let the provider determine what to do for each keymap, such as `goto_location` and `toggle_preview`. This would allow the zk extension[1] to work properly without having to override existing functions (bad practice). [1]: https://github.com/mickael-menu/zk-nvim/discussions/134 --- CHANGELOG.md | 2 + README.md | 63 ++++++++++++++- lua/outline/config.lua | 6 +- lua/outline/providers/norg.lua | 135 +++++++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 lua/outline/providers/norg.lua 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