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
This commit is contained in:
hedy
2023-11-22 22:16:19 +08:00
parent 8c5c69feb2
commit d35187ef37
4 changed files with 201 additions and 5 deletions

View File

@@ -73,6 +73,8 @@
code) code)
- Highlights will also take into account `ctermfg/bg` when setting default values. - Highlights will also take into account `ctermfg/bg` when setting default values.
This ensures outline.nvim highlights work if `termguicolors` is not enabled 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 ### Fixes

View File

@@ -41,6 +41,7 @@ Table of contents
* [Installation](#installation) * [Installation](#installation)
* [Setup](#setup) * [Setup](#setup)
* [Configuration](#configuration) * [Configuration](#configuration)
* [Providers](#providers)
* [Commands](#commands) * [Commands](#commands)
* [Default keymaps](#default-keymaps) * [Default keymaps](#default-keymaps)
* [Highlights](#highlights) * [Highlights](#highlights)
@@ -59,7 +60,7 @@ Table of contents
- Neovim 0.7+ - Neovim 0.7+
- To use modifiers on [commands](#commands), Neovim 0.8 is required. - To use modifiers on [commands](#commands), Neovim 0.8 is required.
Everything else works with Neovim 0.7. 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 ## Installation
@@ -333,7 +334,7 @@ Pass a table to the setup call with your configuration options.
}, },
providers = { providers = {
priority = { 'lsp', 'coc', 'markdown' }, priority = { 'lsp', 'coc', 'markdown', 'norg' },
lsp = { lsp = {
-- Lsp client names to ignore -- Lsp client names to ignore
blacklist_clients = {}, 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. 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.<item>"` to form an import
path, for use as a provider.
External providers from plugins should define the provider module at
`lua/outline/providers/<name>.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 ## Commands
- **:Outline[!]** (✓ bang ✓ mods) - **:Outline[!]** (✓ bang ✓ mods)

View File

@@ -89,7 +89,7 @@ M.defaults = {
up_and_jump = '<C-k>', up_and_jump = '<C-k>',
}, },
providers = { providers = {
priority = { 'lsp', 'coc', 'markdown' }, priority = { 'lsp', 'coc', 'markdown', 'norg' },
lsp = { lsp = {
blacklist_clients = {}, blacklist_clients = {},
}, },
@@ -211,8 +211,8 @@ end
---@return boolean include ---@return boolean include
function M.should_include_symbol(kind, bufnr) function M.should_include_symbol(kind, bufnr)
local ft = vim.api.nvim_buf_get_option(bufnr, 'ft') local ft = vim.api.nvim_buf_get_option(bufnr, 'ft')
-- There can only be one kind in markdown as of now -- There can only be one kind in markdown and norg as of now
if ft == 'markdown' or kind == nil then if ft == 'markdown' or ft == 'norg' or kind == nil then
return true return true
end end

View File

@@ -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