Skip to content

Commit

Permalink
feat: Norg provider and support of external providers
Browse files Browse the repository at this point in the history
- 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]: zk-org/zk-nvim#134
  • Loading branch information
hedyhli committed Nov 22, 2023
1 parent 8c5c69f commit d35187e
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
63 changes: 61 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Table of contents
* [Installation](#installation)
* [Setup](#setup)
* [Configuration](#configuration)
* [Providers](#providers)
* [Commands](#commands)
* [Default keymaps](#default-keymaps)
* [Highlights](#highlights)
Expand All @@ -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

Expand Down Expand Up @@ -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 = {},
Expand Down Expand Up @@ -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.<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

- **:Outline[!]** (✓ bang ✓ mods)
Expand Down
6 changes: 3 additions & 3 deletions lua/outline/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ M.defaults = {
up_and_jump = '<C-k>',
},
providers = {
priority = { 'lsp', 'coc', 'markdown' },
priority = { 'lsp', 'coc', 'markdown', 'norg' },
lsp = {
blacklist_clients = {},
},
Expand Down Expand Up @@ -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

Expand Down
135 changes: 135 additions & 0 deletions lua/outline/providers/norg.lua
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d35187e

Please sign in to comment.