feat!: (BREAKING PLEASE READ) use queries for determining context (#198)

This plugin has been significantly rewritten to use Treesitter
queries instead of patterns for determining context regions for
languages.

The main benefits of this change are:
- it is a much simpler implementation since we can leverage core APIs.
- it fits in more generally with the Treesitter eco-system.
- it allows configuration of contexts to be provided from multiples sources.
- it allows more sophisticated configuration of contexts since queries
  (with directives and predicates) are much more powerful than patterns.
- the query format should be usable for other editors.

The major downside of this new implementation is that it requires each
language to provide it's own query as opposed to using the general
purpose patterns. This means that some languages which had contexts
before may not have them now. If this is the case then please raise an
issue. Adding queries for a specific language is fairly simple but too
much work to implement for all 170+ parsers that exist.

This commits provides explicit support for:
  - bash
  - c
  - cpp
  - typescript
  - rust
  - json
  - lua
  - markdown
  - python
  - yaml
  - php
  - scala
  - teal
  - toml
  - vim

Please see the README for instructions on how to add support for other
languages.

This commit also drops explicit support for Nvim 0.7. If you still need
support for this version then you can use the `compat/0.7` release.
This commit is contained in:
Lewis Russell 2023-03-08 13:17:20 +00:00 committed by GitHub
parent 895ec44f5c
commit 6e53eecca4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1142 additions and 507 deletions

View file

@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: true
matrix:
neovim_branch: ['v0.7.0']
neovim_branch: ['v0.8.2']
runs-on: ubuntu-latest
env:
NEOVIM_BRANCH: ${{ matrix.neovim_branch }}

View file

@ -16,26 +16,21 @@ $(NEOVIM):
nvim-treesitter:
git clone --depth 1 https://github.com/nvim-treesitter/nvim-treesitter
nvim-treesitter/parser/lua.so: nvim-treesitter $(NEOVIM)
nvim-treesitter/parser/%.so: nvim-treesitter $(NEOVIM)
VIMRUNTIME=$(NEOVIM)/runtime $(NEOVIM)/build/bin/nvim \
--headless \
--clean \
--cmd 'set rtp+=./nvim-treesitter' \
-c "TSInstallSync lua" \
-c "q"
nvim-treesitter/parser/rust.so: nvim-treesitter $(NEOVIM)
VIMRUNTIME=$(NEOVIM)/runtime $(NEOVIM)/build/bin/nvim \
--headless \
--clean \
--cmd 'set rtp+=./nvim-treesitter' \
-c "TSInstallSync rust" \
-c "TSInstallSync $*" \
-c "q"
export VIMRUNTIME=$(PWD)/$(NEOVIM)/runtime
.PHONY: test
test: $(NEOVIM) nvim-treesitter nvim-treesitter/parser/lua.so nvim-treesitter/parser/rust.so
test: $(NEOVIM) nvim-treesitter \
nvim-treesitter/parser/lua.so \
nvim-treesitter/parser/rust.so \
nvim-treesitter/parser/typescript.so
$(NEOVIM)/.deps/usr/bin/busted \
-v \
--lazy \

299
README.md
View file

@ -5,7 +5,7 @@ implemented with [nvim-treesitter](https://github.com/nvim-treesitter/nvim-trees
## Requirements
Neovim >= v0.7.x
Neovim >= v0.8.2
Note: if you need support for Neovim 0.6.x please use the tag `compat/0.6`.
@ -29,11 +29,166 @@ use 'nvim-treesitter/nvim-treesitter-context'
![theme](./static/demo.gif)
### Notes
## Supported languages
This plugins uses the new neovim `WinScrolled` event when available to update its
context window. Make sure to have a recent neovim build to get this behavior. The fallback
behavior is to update its content on `CursorMoved`.
- [x] `bash`
- [x] `c`
- [x] `cpp`
- [x] `typescript`
- [x] `rust`
- [x] `json`
- [x] `lua`
- [x] `markdown`
- [x] `python`
- [x] `yaml`
- [x] `php`
- [x] `scala`
- [x] `teal`
- [x] `toml`
- [x] `vim`
- [ ] `ada`
- [ ] `agda`
- [ ] `arduino`
- [ ] `astro`
- [ ] `beancount`
- [ ] `bibtex`
- [ ] `bicep`
- [ ] `blueprint`
- [ ] `c_sharp`
- [ ] `capnp`
- [ ] `chatito`
- [ ] `clojure`
- [ ] `cmake`
- [ ] `commonlisp`
- [ ] `cooklang`
- [ ] `cpon`
- [ ] `css`
- [ ] `cuda`
- [ ] `d`
- [ ] `dart`
- [ ] `devicetree`
- [ ] `dhall`
- [ ] `dockerfile`
- [ ] `dot`
- [ ] `ebnf`
- [ ] `ecma`
- [ ] `eex`
- [ ] `elixir`
- [ ] `elm`
- [ ] `elsa`
- [ ] `elvish`
- [ ] `embedded_template`
- [ ] `erlang`
- [ ] `fennel`
- [ ] `fish`
- [ ] `foam`
- [ ] `fsh`
- [ ] `func`
- [ ] `fusion`
- [ ] `gdscript`
- [ ] `git_rebase`
- [ ] `gleam`
- [ ] `glimmer`
- [ ] `glsl`
- [ ] `go`
- [ ] `godot_resource`
- [ ] `gomod`
- [ ] `gosum`
- [ ] `gowork`
- [ ] `graphql`
- [ ] `hack`
- [ ] `haskell`
- [ ] `hcl`
- [ ] `heex`
- [ ] `hjson`
- [ ] `hlsl`
- [ ] `hocon`
- [ ] `html`
- [ ] `html_tags`
- [ ] `htmldjango`
- [ ] `http`
- [ ] `ini`
- [ ] `java`
- [ ] `javascript`
- [ ] `jq`
- [ ] `jsdoc`
- [ ] `json5`
- [ ] `jsonc`
- [ ] `jsonnet`
- [ ] `jsx`
- [ ] `julia`
- [ ] `kdl`
- [ ] `kotlin`
- [ ] `lalrpop`
- [ ] `latex`
- [ ] `ledger`
- [ ] `llvm`
- [ ] `m68k`
- [ ] `matlab`
- [ ] `menhir`
- [ ] `mermaid`
- [ ] `meson`
- [ ] `nickel`
- [ ] `nix`
- [ ] `ocaml`
- [ ] `ocaml_interface`
- [ ] `ocamllex`
- [ ] `pascal`
- [ ] `perl`
- [ ] `phpdoc`
- [ ] `pioasm`
- [ ] `po`
- [ ] `poe_filter`
- [ ] `prisma`
- [ ] `proto`
- [ ] `prql`
- [ ] `pug`
- [ ] `ql`
- [ ] `qmldir`
- [ ] `qmljs`
- [ ] `query`
- [ ] `r`
- [ ] `racket`
- [ ] `rasi`
- [ ] `rego`
- [ ] `rnoweb`
- [ ] `ron`
- [ ] `rst`
- [ ] `ruby`
- [ ] `scheme`
- [ ] `scss`
- [ ] `slint`
- [ ] `smali`
- [ ] `smithy`
- [ ] `solidity`
- [ ] `sparql`
- [ ] `sql`
- [ ] `starlark`
- [ ] `supercollider`
- [ ] `surface`
- [ ] `svelte`
- [ ] `swift`
- [ ] `sxhkdrc`
- [ ] `t32`
- [ ] `terraform`
- [ ] `thrift`
- [ ] `tiger`
- [ ] `tlaplus`
- [ ] `todotxt`
- [ ] `tsx`
- [ ] `turtle`
- [ ] `twig`
- [ ] `ungrammar`
- [ ] `v`
- [ ] `vala`
- [ ] `verilog`
- [ ] `vhs`
- [ ] `vue`
- [ ] `wgsl`
- [ ] `wgsl_bevy`
- [ ] `yang`
- [ ] `yuck`
- [ ] `zig`
## Configuration
@ -41,94 +196,17 @@ behavior is to update its content on `CursorMoved`.
```lua
require'treesitter-context'.setup{
enable = true, -- Enable this plugin (Can be enabled/disabled later via commands)
max_lines = 0, -- How many lines the window should span. Values <= 0 mean no limit.
trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer'
min_window_height = 0, -- Minimum editor window height to enable context. Values <= 0 mean no limit.
patterns = { -- Match patterns for TS nodes. These get wrapped to match at word boundaries.
-- For all filetypes
-- Note that setting an entry here replaces all other patterns for this entry.
-- By setting the 'default' entry below, you can control which nodes you want to
-- appear in the context window.
default = {
'class',
'function',
'method',
'for',
'while',
'if',
'switch',
'case',
'interface',
'struct',
'enum',
},
-- Patterns for specific filetypes
-- If a pattern is missing, *open a PR* so everyone can benefit.
tex = {
'chapter',
'section',
'subsection',
'subsubsection',
},
haskell = {
'adt'
},
rust = {
'impl_item',
},
terraform = {
'block',
'object_elem',
'attribute',
},
scala = {
'object_definition',
},
vhdl = {
'process_statement',
'architecture_body',
'entity_declaration',
},
markdown = {
'section',
},
elixir = {
'anonymous_function',
'arguments',
'block',
'do_block',
'list',
'map',
'tuple',
'quoted_content',
},
json = {
'pair',
},
typescript = {
'export_statement',
},
yaml = {
'block_mapping_pair',
},
},
exact_patterns = {
-- Example for a specific filetype with Lua patterns
-- Treat patterns.rust as a Lua pattern (i.e "^impl_item$" will
-- exactly match "impl_item" only)
-- rust = true,
},
-- [!] The options below are exposed but shouldn't require your attention,
-- you can safely ignore them.
zindex = 20, -- The Z-index of the context window
mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline'
-- Separator between context and content. Should be a single character string, like '-'.
-- When separator is set, the context will only show up when there are at least 2 lines above cursorline.
separator = nil,
enable = true, -- Enable this plugin (Can be enabled/disabled later via commands)
max_lines = 0, -- How many lines the window should span. Values <= 0 mean no limit.
min_window_height = 0, -- Minimum editor window height to enable context. Values <= 0 mean no limit.
line_numbers = true,
multiline_threshold = 20, -- Maximum number of lines to collapse for a single context line
trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer'
mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline'
-- Separator between context and content. Should be a single character string, like '-'.
-- When separator is set, the context will only show up when there are at least 2 lines above cursorline.
separator = nil,
zindex = 20, -- The Z-index of the context window
}
```
@ -151,3 +229,38 @@ However, you can use this to create a border by applying an underline highlight,
```vim
hi TreesitterContextBottom gui=underline guisp=Grey
```
## Adding support for other languages
To add support for another language, simply add a `context.scm` file under
`queries/[LANG]`.
Queries specify the `@context` capture which specifies the first line of a node
will be used for the context.
Here is a basic example for C:
```query
(function_definition) @context
(for_statement) @context
(if_statement) @context
(while_statement) @context
(do_statement) @context
```
You can easily look at a node names of a tree using `InspectTree` in Nvim 0.9.
Additionally an optional `@context.end` capture can also be specified. When
provided, the text from the start of the `@context` capture to the start of
`@context.end` capture (exclusive) will be used for the context and joined into
a single line.
Here's what that looks like for C:
```query
(if_statement consequence: (_ (_) @context.end)) @context
```
This query specifies that everything from the `if` keyword up-to the first
statement (exclusive) should be used for the context. This is useful when an
if-statement spans multiple lines.

View file

@ -1,15 +1,25 @@
local api = vim.api
local ts_utils = require'nvim-treesitter.ts_utils'
local highlighter = vim.treesitter.highlighter
local parsers = require'nvim-treesitter.parsers'
local augroup = api.nvim_create_augroup
local command = api.nvim_create_user_command
local function word_pattern(p)
return '%f[%w]' .. p .. '%f[^%w]'
end
---@diagnostic disable:invisible
--- @class Config
--- @field enable boolean
--- @field max_lines integer
--- @field min_window_height integer
--- @field line_numbers boolean
--- @field multiline_threshold integer
--- @field trim_scope 'outer'|'inner'
--- @field zindex integer
--- @field mode 'cursor'|'topline'
--- @field separator string?
--- @type Config
local defaultConfig = {
enable = true,
max_lines = 0, -- no limit
@ -22,275 +32,118 @@ local defaultConfig = {
separator = nil,
}
--- @type Config
local config = {}
-- Constants
-- Tells us at which node type to stop when highlighting a multi-line
-- node. If not specified, the highlighting stops after the first line.
local last_nodes
local QUERY_FIELD_NAME = 1
local QUERY_NODE_TYPE = 2
do
local function f(name)
return {
name = name,
kind = QUERY_FIELD_NAME,
}
end
local function t(name)
return {
name = name,
kind = QUERY_NODE_TYPE,
}
end
last_nodes = {
[word_pattern('function')] = {
c = { f'declarator' },
cpp = { f'declarator' },
lua = { f'parameters' },
teal = { f'signature' },
python = { f'return_type', f'parameters' },
rust = { f'return_type', f'parameters' },
javascript = { f'parameters' },
typescript = { f'return_type', f'parameters' },
},
[word_pattern('method')] = {
lua = { f'parameters' },
javascript = { f'parameters' },
typescript = { f'return_type', f'parameters' },
},
[word_pattern('class')] = {
cpp = { t'base_class_clause', f'name' },
python = { f'superclasses' },
}
}
end
-- Tells us which leading child node type to skip when highlighting a
-- multi-line node.
local skip_leading_types = {
[word_pattern('class')] = {
php = 'attribute_list',
},
[word_pattern('method')] = {
php = 'attribute_list',
},
}
-- There are language-specific
local DEFAULT_TYPE_PATTERNS = {
-- These catch most generic groups, eg "function_declaration" or "function_block"
default = {
'class',
'function',
'method',
'for',
'while',
'if',
'switch',
'case',
'interface',
'struct',
'enum',
},
elixir = {
'anonymous_function',
'arguments',
'block',
'do_block',
'list',
'map',
'tuple',
'quoted_content',
},
haskell = {
'adt'
},
json = {
'pair',
},
markdown = {
'section',
},
python = {
'with_statement',
},
rust = {
'impl_item',
},
scala = {
'object_definition',
},
terraform = {
'block',
'object_elem',
'attribute',
},
tex = {
'chapter',
'section',
'subsection',
'subsubsection',
},
typescript = {
'export_statement',
},
verilog = {
'always_construct',
'statement_or_null',
},
vhdl = {
'process_statement',
'architecture_body',
'entity_declaration',
},
yaml = {
'block_mapping_pair',
},
exact_patterns = {},
}
local DEFAULT_TYPE_EXCLUDE_PATTERNS = {
default = {},
teal = {
'function_body',
},
}
local INDENT_PATTERN = '^%s+'
-- Script variables
local did_setup = false
local enabled = false
local gutter_winid, context_winid
local gutter_bufnr, context_bufnr -- Don't access directly, use get_bufs()
-- Don't access directly, use get_bufs()
--- @type integer?
local gutter_winid
--- @type integer?
local context_winid
--- @type integer?
local gutter_bufnr
--- @type integer?
local context_bufnr
local ns = api.nvim_create_namespace('nvim-treesitter-context')
--- @type TSNode[]?
local previous_nodes
--- @return TSNode
local function get_root_node()
---@diagnostic disable-next-line
local tree = parsers.get_parser():parse()[1]
return tree:root()
end
local function is_excluded(node, filetype)
local node_type = node:type()
for _, rgx in ipairs(config.exclude_patterns.default) do
if node_type:find(rgx) then
return true
end
end
local filetype_patterns = config.exclude_patterns[filetype]
for _, rgx in ipairs(filetype_patterns or {}) do
if node_type:find(rgx) then
return true
end
end
return false
end
--- @param node TSNode
--- @param query Query
--- @return Range4?
local function is_valid(node, query)
local bufnr = api.nvim_get_current_buf()
local range --[[@type Range4]] = {node:range()}
range[3] = range[1]
range[4] = -1
local function is_valid(node, filetype)
if is_excluded(node, filetype) then
return false
end
-- Try and iterate on the parent node as iter_matches won't match on the top
-- level node
local iter_node = node:parent() or node
local node_type = node:type()
for _, rgx in ipairs(config.patterns.default) do
if node_type:find(rgx) then
return true
end
end
local filetype_patterns = config.patterns[filetype]
for _, rgx in ipairs(filetype_patterns or {}) do
if node_type:find(rgx) then
return true
end
end
return false
end
for _, match in query:iter_matches(iter_node, bufnr, 0, -1) do
local r = false
local function get_type_pattern(node, type_patterns)
local node_type = node:type()
for _, rgx in ipairs(type_patterns) do
if node_type:find(rgx) then
return rgx
end
end
end
for id, node0 in pairs(match --[[@as table<integer,TSNode>]]) do
local srow, scol, erow, ecol = node0:range()
local function find_node(node, query)
if query.kind == QUERY_FIELD_NAME then
local fields = node:field(query.name)
if fields and fields[1] then
return fields[1]
end
elseif query.kind == QUERY_NODE_TYPE then
local children = ts_utils.get_named_children(node)
for _, c in ipairs(children) do
if c:type() == query.name then
return c
end
end
end
end
local function get_text_for_node(node)
local type = get_type_pattern(node, config.patterns.default) or node:type()
local filetype = vim.bo.filetype
local start_row, start_col = node:start()
local end_row, end_col = node:end_()
local node_text = vim.treesitter.query.get_node_text(node, 0)
if node_text == nil then return nil, nil end
local lines = vim.split(node_text, '\n')
if start_col ~= 0 then
lines[1] = api.nvim_buf_get_lines(0, start_row, start_row + 1, false)[1]
end
start_col = 0
local queries = (last_nodes[type] or {})[filetype]
local last_position
if queries then
local child
for _, q in ipairs(queries) do
local n = find_node(node, q)
if n then
child = n
-- because iter_node != node we could match outside of node
if srow < range[1] then
break
end
local name = query.captures[id] -- name of the capture in the query
if not r and name == 'context' then
r = node == node0
elseif name == 'context.final' then
range[3] = erow
range[4] = ecol
elseif name == 'context.end' then
range[3] = srow
range[4] = scol
end
end
if child then
last_position = {child:end_()}
end_row = last_position[1]
end_col = last_position[2]
local last_index = end_row - start_row
lines = vim.list_slice(lines, 1, last_index + 1)
lines[#lines] = lines[#lines]:sub(1, end_col)
if r then
return range
end
end
end
if not last_position or #lines > config.multiline_threshold then
--- @param range Range4
--- @return string[]?, Range4?
local function get_text_for_range(range)
if range[4] == 0 then
range[3] = range[3] - 1
range[4] = -1
end
local lines = api.nvim_buf_get_text(0, range[1], 0, range[3], range[4], {})
if lines == nil then
return nil, nil
end
local start_row = range[1]
local end_row = range[3]
local end_col = range[4]
lines = vim.list_slice(lines, 1, end_row - start_row+1)
lines[#lines] = lines[#lines]:sub(1, end_col)
if #lines > config.multiline_threshold then
lines = vim.list_slice(lines, 1, 1)
end_row = start_row
end_col = #lines[1]
end
local range = {start_row, start_col, end_row, end_col}
range = {start_row, 0, end_row, end_col}
return lines, range
end
-- Merge lines, removing the indentation after 1st line
--- @param lines string[]
--- @return string
local function merge_lines(lines)
local text = { lines[1] }
for i = 2, #lines do
@ -300,8 +153,13 @@ local function merge_lines(lines)
end
-- Get indentation for lines except first
--- @param lines string[]
--- @return integer[]
local function get_indents(lines)
--- @type integer[]
--- @diagnostic disable-next-line
local indents = vim.tbl_map(function(line)
--- @type string?
local indent = line:match(INDENT_PATTERN)
return indent and #indent or 0
end, lines)
@ -310,13 +168,14 @@ local function get_indents(lines)
return indents
end
--- @return integer
local function get_gutter_width()
return vim.fn.getwininfo(vim.api.nvim_get_current_win())[1].textoff
end
local cursor_moved_vertical
local cursor_moved_vertical --[[@type fun(): boolean]]
do
local line
local line --[[@type integer]]
cursor_moved_vertical = function()
local newline = vim.api.nvim_win_get_cursor(0)[1]
if newline ~= line then
@ -327,6 +186,7 @@ do
end
end
--- @return integer, integer
local function get_bufs()
if not context_bufnr or not api.nvim_buf_is_valid(context_bufnr) then
context_bufnr = api.nvim_create_buf(false, true)
@ -351,6 +211,14 @@ local function delete_bufs()
gutter_bufnr = nil
end
--- @param bufnr integer
--- @param winid integer?
--- @param width integer
--- @param height integer
--- @param col integer
--- @param ty string
--- @param hl string
--- @return integer
local function display_window(bufnr, winid, width, height, col, ty, hl)
if not winid or not api.nvim_win_is_valid(winid) then
local sep = config.separator
@ -389,6 +257,34 @@ local M = {
config = config,
}
--- @param node TSNode?
--- @return TSNode[]
local function get_node_parents(node)
-- save nodes in a table to iterate from top to bottom
--- @type TSNode[]
local parents = {}
while node ~= nil do
parents[#parents+1] = node
node = node:parent()
end
return parents
end
--- @return integer, integer
local function get_pos()
--- @type integer, integer
local lnum, col
if config.mode == 'topline' then
lnum, col = vim.fn.line('w0') --[[@as integer]], 0
else -- default to 'cursor'
lnum, col = unpack(api.nvim_win_get_cursor(0)) --[[@as integer]]
end
return lnum, col
end
--- @param max_lines integer
--- @return Range4[]?
local function get_parent_matches(max_lines)
if max_lines == 0 then
return
@ -399,14 +295,31 @@ local function get_parent_matches(max_lines)
end
local root_node = get_root_node()
local lnum, col
if config.mode == 'topline' then
lnum, col = vim.fn.line('w0'), 0
else -- default to 'cursor'
lnum, col = unpack(api.nvim_win_get_cursor(0))
--- @type string
local lang = parsers.ft_to_lang(vim.bo.filetype)
local ok, query = pcall(vim.treesitter.query.get_query, lang, 'context')
if not ok then
vim.notify_once(
string.format('Unable to load context query for %s:\n%s', lang, query),
vim.log.levels.ERROR,
{ title = 'nvim-treesitter-context' }
)
return
end
if not query then
return
end
local lnum, col = get_pos()
--- @type Range4[]
local last_matches
--- @type Range4[]
local parent_matches = {}
local line_offset = 0
@ -423,25 +336,19 @@ local function get_parent_matches(max_lines)
local topline = vim.fn.line('w0')
-- save nodes in a table to iterate from top to bottom
local parents = {}
while node ~= nil do
parents[#parents+1] = node
node = node:parent()
end
local parents = get_node_parents(node)
for i = #parents, 1, -1 do
local parent = parents[i]
local row = parent:start()
local height = math.min(max_lines, #parent_matches)
if is_valid(parent, vim.bo.filetype)
and row >= 0
and row < (topline + height - 1) then
local range = is_valid(parent, query)
if range and row >= 0 and row < (topline + height - 1) then
if row == last_row then
parent_matches[#parent_matches] = parent
parent_matches[#parent_matches] = range
else
table.insert(parent_matches, parent)
parent_matches[#parent_matches+1] = range
last_row = row
local new_height = math.min(max_lines, #parent_matches)
@ -469,6 +376,9 @@ local function get_parent_matches(max_lines)
end
end
--- @generic F: function
--- @param fn F
--- @return F
local function throttle_fn(fn)
local recalc_after_cooldown = false
local cooling_down = false
@ -495,7 +405,6 @@ local function throttle_fn(fn)
return wrapped
end
local function close()
previous_nodes = nil
-- Can't close other windows when the command-line window is open
@ -514,6 +423,9 @@ local function close()
gutter_winid = nil
end
--- @param bufnr integer
--- @param lines string[]
--- @return boolean
local function set_lines(bufnr, lines)
local clines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
local redraw = false
@ -536,10 +448,13 @@ local function set_lines(bufnr, lines)
return redraw
end
--- @param bufnr integer
--- @param ctx_bufnr integer
--- @param contexts Context[]
local function highlight_contexts(bufnr, ctx_bufnr, contexts)
api.nvim_buf_clear_namespace(ctx_bufnr, ns, 0, -1)
local buf_highlighter = highlighter.active[bufnr]
local buf_highlighter = highlighter.active[bufnr] --[[@as TSHighlighter]]
if not buf_highlighter then
-- Use standard highlighting when TS highlighting is not available
@ -558,26 +473,26 @@ local function highlight_contexts(bufnr, ctx_bufnr, contexts)
local buf_query = buf_highlighter:get_query(parsers.ft_to_lang(vim.bo.filetype))
local query = buf_query:query()
local query = assert(buf_query:query())
local root = get_root_node()
for i, context in ipairs(contexts) do
local start_row, _, end_row, end_col = unpack(context.range)
local start_row = context.range[1]
local end_row = context.range[3]
local end_col = context.range[4]
local indents = context.indents
local lines = context.lines
local start_row_abs = context.node:start()
for capture, node in query:iter_captures(root, bufnr, start_row, context.node:end_()) do
for capture, node in query:iter_captures(root, bufnr, start_row, end_row + 1) do
local node_start_row, node_start_col, node_end_row, node_end_col = node:range()
if node_end_row > end_row or
(node_end_row == end_row and node_end_col > end_col) then
(node_end_row == end_row and node_end_col > end_col and end_col ~= -1) then
break
end
if node_start_row >= start_row_abs then
local intended_start_row = node_start_row - start_row_abs
if node_start_row >= start_row then
local intended_start_row = node_start_row - start_row
-- Add 1 for each space added between lines when
-- we replace '\n' with ' '
@ -600,10 +515,15 @@ local function highlight_contexts(bufnr, ctx_bufnr, contexts)
end
end
--- @param lnum integer
--- @param width integer
--- @return string
local function build_lno_str(lnum, width)
return string.format('%'..width..'d', lnum)
end
--- @param ctx_node_line_num integer
--- @return integer
local function get_relative_line_num(ctx_node_line_num)
local cursor_line_num = vim.fn.line('.')
local num_folded_lines = 0
@ -635,30 +555,18 @@ local function horizontal_scroll_contexts()
end
end
local function normalize_node(node)
local type = get_type_pattern(node, config.patterns.default) or node:type()
local filetype = vim.bo.filetype
--- @class Context
--- @field indents integer[]
--- @field lines string[]
--- @field range Range4
local skip_leading_type = (skip_leading_types[type] or {})[filetype]
if skip_leading_type then
local children = ts_utils.get_named_children(node)
for _, child in ipairs(children) do
if child:type() ~= skip_leading_type then
node = child
break
end
end
end
return node
end
local function open(ctx_nodes)
--- @param ctx_ranges Range4[]
local function open(ctx_ranges)
local bufnr = api.nvim_get_current_buf()
local gutter_width = get_gutter_width()
local win_width = math.max(1, api.nvim_win_get_width(0) - gutter_width)
local win_height = math.max(1, #ctx_nodes)
local win_height = math.max(1, #ctx_ranges)
local gbufnr, ctx_bufnr = get_bufs()
@ -674,19 +582,18 @@ local function open(ctx_nodes)
-- Set text
local context_text = {}
local lno_text = {}
local contexts = {}
local context_text --[[@type string[] ]] = {}
local lno_text --[[@type string[] ]] = {}
local contexts --[[@type Context[] ]] = {}
for _, node in ipairs(ctx_nodes) do
node = normalize_node(node)
local lines, range = get_text_for_node(node)
if lines == nil or range == nil or range[1] == nil then return end
for _, range0 in ipairs(ctx_ranges) do
local lines, range = get_text_for_range(range0)
if lines == nil or range == nil or range[1] == nil then
return
end
local text = merge_lines(lines)
contexts[#contexts+1] = {
node = node,
lines = lines,
range = range,
indents = get_indents(lines),
@ -694,7 +601,7 @@ local function open(ctx_nodes)
table.insert(context_text, text)
local line_num
local line_num --[[@type integer]]
local ctx_line_num = range[1] + 1
if vim.o.relativenumber then
line_num = get_relative_line_num(ctx_line_num)
@ -710,13 +617,14 @@ local function open(ctx_nodes)
return
end
highlight_contexts(bufnr, ctx_bufnr, contexts)
api.nvim_buf_set_extmark(ctx_bufnr, ns, #lno_text-1, 0, {end_line=#lno_text, hl_group='TreesitterContextBottom', hl_eol=true})
api.nvim_buf_set_extmark(gbufnr, ns, #context_text-1, 0, {end_line=#context_text, hl_group='TreesitterContextBottom', hl_eol=true})
end
--- @param config_max integer
--- @return integer
local function calc_max_lines(config_max)
local max_lines = config_max
max_lines = max_lines == 0 and -1 or max_lines
@ -765,9 +673,12 @@ local update = throttle_fn(function()
end
end)
--- @param group string
--- @return function
local function autocmd_for_group(group)
local gid = augroup(group, {})
return function(event, opts)
---@diagnostic disable:no-unknown
if opts then
if type(opts) == 'function' then
opts = { callback = opts }
@ -826,17 +737,7 @@ function M.setup(options)
local userOptions = options or {}
config = vim.tbl_deep_extend('force', {}, defaultConfig, userOptions)
config.patterns = vim.tbl_deep_extend('force', {}, DEFAULT_TYPE_PATTERNS, userOptions.patterns or {})
config.exclude_patterns = vim.tbl_deep_extend('force', {}, DEFAULT_TYPE_EXCLUDE_PATTERNS, userOptions.exclude_patterns or {})
config.exact_patterns = vim.tbl_deep_extend('force', {}, userOptions.exact_patterns or {})
for filetype, patterns in pairs(config.patterns) do
-- Map with word_pattern only if users don't need exact pattern matching
if not config.exact_patterns[filetype] then
config.patterns[filetype] = vim.tbl_map(word_pattern, patterns)
end
end
config = vim.tbl_deep_extend('force', {}, defaultConfig, userOptions)
if config.enable then
M.enable()

6
queries/bash/context.scm Normal file
View file

@ -0,0 +1,6 @@
([
(for_statement)
(function_definition)
(if_statement)
] @context)

28
queries/c/context.scm Normal file
View file

@ -0,0 +1,28 @@
(function_definition
body: (_ (_) @context.end)
) @context
(for_statement
(compound_statement) @context.end
) @context
(if_statement
consequence: (_ (_) @context.end)
) @context
(while_statement
body: (_ (_) @context.end)
) @context
(do_statement
body: (_ (_) @context.end)
) @context
(struct_specifier
body: (_ (_) @context.end)
) @context
(enum_specifier
body: (_ (_) @context.end)
) @context

5
queries/cpp/context.scm Normal file
View file

@ -0,0 +1,5 @@
; inherits: c
(class_specifier
body: (_ (_) @context.end)
) @context

5
queries/json/context.scm Normal file
View file

@ -0,0 +1,5 @@
([
(object)
(pair)
] @context)

27
queries/lua/context.scm Normal file
View file

@ -0,0 +1,27 @@
(for_statement
body: (_) @context.end
) @context
(while_statement
body: (_) @context.end
) @context
(do_statement
body: (_) @context.end
) @context
(function_definition
body: (_) @context.end
) @context
(table_constructor
(_) @context.end
) @context
(function_declaration
parameters: (_) @context.final
) @context
(if_statement
consequence: (_) @context.end
) @context

View file

@ -0,0 +1,2 @@
((section) @context)

28
queries/php/context.scm Normal file
View file

@ -0,0 +1,28 @@
(function_definition
body: (_ (_) @context.end)
) @context
(while_statement
body: (_ (_) @context.end)
) @context
(if_statement
body: (_ (_) @context.end)
) @context
(do_statement
body: (_ (_) @context.end)
) @context
(foreach_statement
body: (_ (_) @context.end)
) @context
(class_declaration
body: (_ (_) @context.end)
) @context
(for_statement
(compound_statement (_) @context.end)
) @context

View file

@ -0,0 +1,47 @@
(class_definition
body: (_) @context.end
) @context
(function_definition
body: (_) @context.end
) @context
(try_statement
body: (_) @context.end
) @context
(with_statement
body: (_) @context.end
) @context
(if_statement
consequence: (_) @context.end
) @context
(elif_clause
consequence: (_) @context.end
) @context
(case_clause
consequence: (_) @context.end
) @context
(while_statement
body: (_) @context.end
) @context
(except_clause
(block) @context.end
) @context
(match_statement
alternative: (_) @context.end
) @context
([
(for_statement)
(finally_clause)
(else_clause)
(pair)
(expression_statement)
] @context)

29
queries/rust/context.scm Normal file
View file

@ -0,0 +1,29 @@
(for_expression
body: (_ (_) @context.end)
) @context
(if_expression
consequence: (_ (_) @context.end)
) @context
(function_item
body: (_ (_) @context.end)
) @context
(impl_item
type: (_) @context.final
) @context
(struct_item
body: (_ (_) @context.end)
) @context
([
(mod_item)
(enum_item)
(closure_expression)
(expression_statement)
(loop_expression)
(match_expression)
] @context)

28
queries/scala/context.scm Normal file
View file

@ -0,0 +1,28 @@
(function_definition
body: (_ (_) @context.end)
) @context
(class_definition
body: (_ (_) @context.end)
) @context
(object_definition
body: (_ (_) @context.end)
) @context
(case_clause
body: (_ (_) @context.end)
) @context
(match_expression
body: (_ (_) @context.end)
) @context
(call_expression
arguments: (_ (_) @context.end)
) @context
(if_expression
consequence: (_ (_) @context.end)
) @context

28
queries/teal/context.scm Normal file
View file

@ -0,0 +1,28 @@
(while_statement) @context
(generic_for_statement
body: (_ (_) @context.end)
) @context
(function_statement
body: (_) @context.end
) @context
(anon_function
body: (_) @context.end
) @context
(if_statement
condition: (_)
(_) @context.end
) @context
(elseif_block
condition: (_)
(_) @context.end
) @context
(record_declaration
record_body: (_) @context.end
) @context

5
queries/toml/context.scm Normal file
View file

@ -0,0 +1,5 @@
([
(table)
(pair)
] @context)

View file

@ -0,0 +1,23 @@
(interface_declaration
body: (_ (_) @context.end)
) @context
(class_declaration
body: (_ (_) @context.end)
) @context
(method_definition
body: (_ (_) @context.end)
) @context
(for_statement
body: (_ (_) @context.end)
) @context
(function_declaration
body: (_ (_) @context.end)
) @context
(if_statement
consequence: (_ (_) @context.end)
) @context

36
queries/vim/context.scm Normal file
View file

@ -0,0 +1,36 @@
(if_statement
(body) @context.end
) @context
(elseif_statement
(body) @context.end
) @context
(else_statement
(body) @context.end
) @context
(function_definition
(body) @context.end
) @context
(while_loop
(body) @context.end
) @context
(for_loop
(body) @context.end
) @context
(try_statement
(body) @context.end
) @context
(catch_statement
(body) @context.end
) @context
(finally_statement
(body) @context.end
) @context

5
queries/yaml/context.scm Normal file
View file

@ -0,0 +1,5 @@
([
(block_mapping)
(block_mapping_pair)
(block_sequence_item)
] @context)

76
test/test.c Normal file
View file

@ -0,0 +1,76 @@
struct Bert {
int *f1;
// comment
int *f2;
// comment
// comment
// comment
// comment
// comment
};
typedef enum {
E1,
E2,
E3
// comment
// comment
// comment
// comment
// comment
// comment
} Myenum;
int main(int arg1,
char **arg2,
char **arg3
)
{
if (arg1 == 4
&& arg2 == arg3) {
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
// comment
for (int i = 0; i < arg1; i++) {
// comment
// comment
// comment
// comment
while (1) {
// comment
// comment
// comment
// comment
// comment
}
do {
// comment
// comment
// comment
// comment
// comment
} while (1);
// comment
// comment
// comment
// comment
// comment
}
}
}

83
test/test.php Normal file
View file

@ -0,0 +1,83 @@
<?php
/*
* comment
*/
function foo($a, $b) {
//loop, between low & high
while ($a <= $b) {
// comment
$index = $low + floor(($high - $low) * $delta);
// comment
$indexValue = $a;
if ($indexValue === $a) {
// comment
$position = $index;
return (int) $position;
}
if ($indexValue < $key) {
// comment
$low = $index + 1;
}
if ($indexValue > $key) {
// comment
do {
// comment
echo "The number is: $x <br>";
$x++;
} while ($x <= 5);
for ($x = 0; $x <= 10; $x++) {
echo "The number is: $x <br>";
}
foreach ($colors as $value) {
echo "$value <br>";
}
$high = $index - 1;
}
}
//when key not found in array or array not sorted
return null;
}
class Fruit {
// comment
}

View file

@ -1,12 +1,28 @@
impl Foo {
fn bar(&self) {
if condition {
for i in 0..100 {
// comment
}
}
}
@ -14,4 +30,12 @@ impl Foo {
struct Foo {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

47
test/test.ts Normal file
View file

@ -0,0 +1,47 @@
interface User {
name: string;
id: number;
}
 
class UserAccount {
name: string;
id: number;
 
constructor(name: string, id: number) {
this.name = name;
this.id = id;
for (let i = 0; i < 3; i++) {
console.log("hello");
}
}
}
function wrapInArray(obj: string | string[]) {
if (typeof obj === "string") {
return [obj];
}
return obj;
}

View file

@ -3,15 +3,16 @@ local Screen = require('test.functional.ui.screen')
local clear = helpers.clear
local exec_lua = helpers.exec_lua
local eq = helpers.eq
local cmd = helpers.command
local feed = helpers.feed
describe('ts_context', function()
local screen
setup(function()
before_each(function()
clear()
screen = Screen.new(30, 16)
screen:attach()
screen:set_default_attr_ids({
[1] = {foreground = Screen.colors.Brown, background = Screen.colors.LightMagenta, bold = true};
[2] = {background = Screen.colors.LightMagenta};
@ -19,12 +20,13 @@ describe('ts_context', function()
[4] = {bold = true, foreground = Screen.colors.Brown};
[5] = {foreground = Screen.colors.DarkCyan};
[6] = {bold = true, foreground = Screen.colors.Blue};
[7] = {foreground = Screen.colors.SeaGreen, background = Screen.colors.LightMagenta, bold = true};
[8] = {foreground = Screen.colors.Blue};
[9] = {bold = true, foreground = Screen.colors.SeaGreen};
[10] = {foreground = Screen.colors.Fuchsia, background = Screen.colors.LightMagenta};
[11] = {foreground = Screen.colors.Fuchsia};
[12] = {foreground = tonumber('0x6a0dad'), background = Screen.colors.LightMagenta};
})
end)
before_each(function()
clear()
screen:attach()
cmd [[set runtimepath+=.,./nvim-treesitter]]
cmd [[let $XDG_CACHE_HOME='scratch/cache']]
cmd [[set packpath=]]
@ -88,99 +90,191 @@ describe('ts_context', function()
]]}
end)
it('edit a file in topline mode', function()
exec_lua[[require'treesitter-context'.setup{
mode = 'topline',
max_lines = 2,
}]]
cmd('edit test/nested_file.rs')
feed'L'
feed'<C-e>'
-- screen:snapshot_util()
screen:expect{grid=[[
{1:impl}{2: Foo { }|
{4:fn} {5:bar}({7:&}{8:self}) { |
{4:if} condition { |
|
|
{4:for} i {4:in} {8:0}..{8:100} { |
|
|
} |
} |
} |
} |
|
{4:^struct} {5:Foo} { |
|
|
]], attr_ids={
[1] = {foreground = Screen.colors.Brown, background = Screen.colors.Plum1, bold = true};
[2] = {background = Screen.colors.Plum1};
[3] = {foreground = Screen.colors.Cyan4, background = Screen.colors.Plum1};
[4] = {bold = true, foreground = Screen.colors.Brown};
[5] = {foreground = Screen.colors.Cyan4};
[6] = {bold = true, foreground = Screen.colors.Blue1};
[7] = {bold = true, foreground = Screen.colors.SeaGreen4};
[8] = {foreground = Screen.colors.Fuchsia};
}}
describe('language:', function()
before_each(function()
exec_lua[[require'treesitter-context'.setup{
mode = 'topline',
}]]
cmd'set scrolloff=5'
cmd'set nowrap'
end)
feed'<C-e>'
screen:expect{grid=[[
{2: }{1:fn}{2: }{3:bar}{2:(}{7:&}{8:self}{2:) }|
{2: }{1:if}{2: condition { }|
|
|
{4:for} i {4:in} {9:0}..{9:100} { |
|
|
} |
} |
} |
} |
|
{4:^struct} {5:Foo} { |
|
} |
|
]], attr_ids={
[1] = {foreground = Screen.colors.Brown, bold = true, background = Screen.colors.LightMagenta};
[2] = {background = Screen.colors.LightMagenta};
[3] = {background = Screen.colors.LightMagenta, foreground = Screen.colors.Cyan4};
[4] = {foreground = Screen.colors.Brown, bold = true};
[5] = {foreground = Screen.colors.Cyan4};
[6] = {foreground = Screen.colors.Blue1, bold = true};
[7] = {foreground = Screen.colors.SeaGreen4, bold = true, background = Screen.colors.LightMagenta};
[8] = {background = Screen.colors.LightMagenta, foreground = Screen.colors.Magenta1};
[9] = {foreground = Screen.colors.Magenta1};
}}
it('rust', function()
cmd('edit test/test.rs')
feed'20<C-e>'
screen:expect{grid=[[
{1:impl}{2: Foo }|
{2: }{1:fn}{2: }{3:bar}{2:(}{7:&}{10:self}{2:) { }|
{2: }{1:if}{2: condition { }|
{2: }{1:for}{2: i }{1:in}{2: }{10:0}{2:..}{10:100}{2: { }|
|
^ } |
} |
} |
} |
|
{4:struct} {5:Foo} { |
|
active: {9:bool}, |
|
username: {9:String}, |
|
]]}
feed'14<C-e>'
screen:expect{grid=[[
{1:struct}{2: }{3:Foo}{2: { }|
|
email: {9:String}, |
|
sign_in_count: {9:u64}, |
^ |
} |
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
|
]]}
end)
it('c', function()
cmd('edit test/test.c')
feed'<C-e>'
-- Check the struct context
screen:expect{grid=[[
{7:struct}{2: Bert { }|
{8:// comment} |
{9:int} *f2; |
{8:// comment} |
{8:// comment} |
^ {8:// comment} |
{8:// comment} |
{8:// comment} |
}; |
|
{9:typedef} {9:enum} { |
E1, |
E2, |
E3 |
{8:// comment} |
|
]]}
feed'12<C-e>'
-- Check the enum context
screen:expect{grid=[[
{7:typedef}{2: }{7:enum}{2: { }|
E3 |
{8:// comment} |
{8:// comment} |
{8:// comment} |
^ {8:// comment} |
{8:// comment} |
{8:// comment} |
} Myenum; |
|
{9:int} main({9:int} arg1, |
{9:char} **arg2, |
{9:char} **arg3 |
) |
{ |
|
]]}
feed'40<C-e>'
screen:expect{grid=[[
{7:int}{2: main(}{7:int}{2: arg1, }{7:char}{2: **arg2}|
{2: }{1:if}{2: (arg1 == }{10:4}{2: && arg2 == arg}|
{2: }{1:for}{2: (}{7:int}{2: i = }{10:0}{2:; i < arg1; }|
{2: }{1:while}{2: (}{10:1}{2:) { }|
} |
^ |
{4:do} { |
{8:// comment} |
{8:// comment} |
{8:// comment} |
{8:// comment} |
{8:// comment} |
|
} {4:while} ({11:1}); |
{8:// comment} |
|
]]}
end)
it('typescript', function()
cmd('edit test/test.ts')
feed'<C-e>'
screen:expect{grid=[[
{1:interface}{2: }{3:User}{2: }{3:{}{2: }|
|
|
|
{5:id}: {9:number}{4:;} |
^ |
|
|
|
{5:}} |
  |
{4:class} UserAccount {5:{} |
{5:name}: {9:string}; |
{5:id}: {9:number}; |
|
|
]]}
feed'21<C-e>'
screen:expect{grid=[[
{1:class}{2: UserAccount }{3:{}{2: }|
{2: }{3:constructor}{2:(}{12:name}{2::}{12: }{7:string}{1:,}{12: id}|
{2: }{1:for}{2: (}{3:let}{2: i = }{10:0}{1:;}{2: i < }{10:3}{1:;}{2: i++}|
|
|
^ |
{5:}} |
|
|
|
|
{5:}} |
{5:}} |
|
|
|
]]}
feed'16<C-e>'
screen:expect{grid=[[
{1:function}{2: }{3:wrapInArray}{2:(}{12:obj}{2::}{12: }{7:stri}|
{2: }{1:if}{2: (}{3:typeof}{2: obj === }{10:"string"}{2:)}|
|
|
|
^ |
{5:}} |
{4:return} obj; |
{5:}} |
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
{6:~ }|
|
]]}
end)
feed'3<C-e>'
screen:expect{grid=[[
{2: }{1:if}{2: condition { }|
{2: }{1:for}{2: i }{1:in}{2: }{7:0}{2:..}{7:100}{2: { }|
|
|
} |
} |
} |
} |
|
{4:^struct} {5:Foo} { |
|
} |
{6:~ }|
{6:~ }|
{6:~ }|
|
]], attr_ids={
[1] = {background = Screen.colors.Plum1, bold = true, foreground = Screen.colors.Brown};
[2] = {background = Screen.colors.Plum1};
[3] = {background = Screen.colors.Plum1, foreground = Screen.colors.Cyan4};
[4] = {foreground = Screen.colors.Brown, bold = true};
[5] = {foreground = Screen.colors.Cyan4};
[6] = {foreground = Screen.colors.Blue1, bold = true};
[7] = {background = Screen.colors.Plum1, foreground = Screen.colors.Magenta};
}}
end)
end)