mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 19:38:20 +00:00

- Added 'spell' option to extmarks: Extmarks with this set will have the region spellchecked. - Added 'noplainbuffer' option to 'spelloptions': This is used to tell Neovim not to spellcheck the buffer. The old behaviour was to spell check the whole buffer unless :syntax was set. - Added spelling support to the treesitter highlighter: @spell captures in highlights.scm are used to define regions which should be spell checked. - Added support for navigating spell errors for extmarks: Works for both ephemeral and static extmarks - Added '_on_spell_nav' callback for decoration providers: Since ephemeral callbacks are only drawn for the visible screen, providers must implement this callback to instruct Neovim which regions in the buffer need can be spell checked. The callback takes a start position and an end position. Note: this callback is subject to change hence the _ prefix. - Added spell captures for built-in support languages Co-authored-by: Lewis Russell <lewis6991@gmail.com> Co-authored-by: Björn Linse <bjorn.linse@gmail.com>
269 lines
6.7 KiB
Lua
269 lines
6.7 KiB
Lua
local a = vim.api
|
|
local query = require('vim.treesitter.query')
|
|
|
|
-- support reload for quick experimentation
|
|
local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {}
|
|
TSHighlighter.__index = TSHighlighter
|
|
|
|
TSHighlighter.active = TSHighlighter.active or {}
|
|
|
|
local TSHighlighterQuery = {}
|
|
TSHighlighterQuery.__index = TSHighlighterQuery
|
|
|
|
local ns = a.nvim_create_namespace('treesitter/highlighter')
|
|
|
|
---@private
|
|
function TSHighlighterQuery.new(lang, query_string)
|
|
local self = setmetatable({}, { __index = TSHighlighterQuery })
|
|
|
|
self.hl_cache = setmetatable({}, {
|
|
__index = function(table, capture)
|
|
local name = self._query.captures[capture]
|
|
local id = 0
|
|
if not vim.startswith(name, '_') then
|
|
id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang)
|
|
end
|
|
|
|
rawset(table, capture, id)
|
|
return id
|
|
end,
|
|
})
|
|
|
|
if query_string then
|
|
self._query = query.parse_query(lang, query_string)
|
|
else
|
|
self._query = query.get_query(lang, 'highlights')
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighterQuery:query()
|
|
return self._query
|
|
end
|
|
|
|
--- Creates a new highlighter using @param tree
|
|
---
|
|
---@param tree The language tree to use for highlighting
|
|
---@param opts Table used to configure the highlighter
|
|
--- - queries: Table to overwrite queries used by the highlighter
|
|
function TSHighlighter.new(tree, opts)
|
|
local self = setmetatable({}, TSHighlighter)
|
|
|
|
if type(tree:source()) ~= 'number' then
|
|
error('TSHighlighter can not be used with a string parser source.')
|
|
end
|
|
|
|
opts = opts or {}
|
|
self.tree = tree
|
|
tree:register_cbs({
|
|
on_changedtree = function(...)
|
|
self:on_changedtree(...)
|
|
end,
|
|
on_bytes = function(...)
|
|
self:on_bytes(...)
|
|
end,
|
|
on_detach = function(...)
|
|
self:on_detach(...)
|
|
end,
|
|
})
|
|
|
|
self.bufnr = tree:source()
|
|
self.edit_count = 0
|
|
self.redraw_count = 0
|
|
self.line_count = {}
|
|
-- A map of highlight states.
|
|
-- This state is kept during rendering across each line update.
|
|
self._highlight_states = {}
|
|
self._queries = {}
|
|
|
|
-- Queries for a specific language can be overridden by a custom
|
|
-- string query... if one is not provided it will be looked up by file.
|
|
if opts.queries then
|
|
for lang, query_string in pairs(opts.queries) do
|
|
self._queries[lang] = TSHighlighterQuery.new(lang, query_string)
|
|
end
|
|
end
|
|
|
|
a.nvim_buf_set_option(self.bufnr, 'syntax', '')
|
|
|
|
TSHighlighter.active[self.bufnr] = self
|
|
|
|
-- Tricky: if syntax hasn't been enabled, we need to reload color scheme
|
|
-- but use synload.vim rather than syntax.vim to not enable
|
|
-- syntax FileType autocmds. Later on we should integrate with the
|
|
-- `:syntax` and `set syntax=...` machinery properly.
|
|
if vim.g.syntax_on ~= 1 then
|
|
vim.api.nvim_command('runtime! syntax/synload.vim')
|
|
end
|
|
vim.bo[self.bufnr].spelloptions = 'noplainbuffer'
|
|
|
|
self.tree:parse()
|
|
|
|
return self
|
|
end
|
|
|
|
--- Removes all internal references to the highlighter
|
|
function TSHighlighter:destroy()
|
|
if TSHighlighter.active[self.bufnr] then
|
|
TSHighlighter.active[self.bufnr] = nil
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:get_highlight_state(tstree)
|
|
if not self._highlight_states[tstree] then
|
|
self._highlight_states[tstree] = {
|
|
next_row = 0,
|
|
iter = nil,
|
|
}
|
|
end
|
|
|
|
return self._highlight_states[tstree]
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:reset_highlight_state()
|
|
self._highlight_states = {}
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end)
|
|
a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1)
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:on_detach()
|
|
self:destroy()
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:on_changedtree(changes)
|
|
for _, ch in ipairs(changes or {}) do
|
|
a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1)
|
|
end
|
|
end
|
|
|
|
--- Gets the query used for @param lang
|
|
---
|
|
---@param lang A language used by the highlighter.
|
|
function TSHighlighter:get_query(lang)
|
|
if not self._queries[lang] then
|
|
self._queries[lang] = TSHighlighterQuery.new(lang)
|
|
end
|
|
|
|
return self._queries[lang]
|
|
end
|
|
|
|
---@private
|
|
local function on_line_impl(self, buf, line, spell)
|
|
self.tree:for_each_tree(function(tstree, tree)
|
|
if not tstree then
|
|
return
|
|
end
|
|
|
|
local root_node = tstree:root()
|
|
local root_start_row, _, root_end_row, _ = root_node:range()
|
|
|
|
-- Only worry about trees within the line range
|
|
if root_start_row > line or root_end_row < line then
|
|
return
|
|
end
|
|
|
|
local state = self:get_highlight_state(tstree)
|
|
local highlighter_query = self:get_query(tree:lang())
|
|
|
|
-- Some injected languages may not have highlight queries.
|
|
if not highlighter_query:query() then
|
|
return
|
|
end
|
|
|
|
if state.iter == nil or state.next_row < line then
|
|
state.iter =
|
|
highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
|
|
end
|
|
|
|
while line >= state.next_row do
|
|
local capture, node, metadata = state.iter()
|
|
|
|
if capture == nil then
|
|
break
|
|
end
|
|
|
|
local start_row, start_col, end_row, end_col = node:range()
|
|
local hl = highlighter_query.hl_cache[capture]
|
|
|
|
local is_spell = highlighter_query:query().captures[capture] == 'spell'
|
|
|
|
if hl and end_row >= line and (not spell or is_spell) then
|
|
a.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
|
|
end_line = end_row,
|
|
end_col = end_col,
|
|
hl_group = hl,
|
|
ephemeral = true,
|
|
priority = tonumber(metadata.priority) or 100, -- Low but leaves room below
|
|
conceal = metadata.conceal,
|
|
spell = is_spell,
|
|
})
|
|
end
|
|
if start_row > line then
|
|
state.next_row = start_row
|
|
end
|
|
end
|
|
end, true)
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter._on_line(_, _win, buf, line, _)
|
|
local self = TSHighlighter.active[buf]
|
|
if not self then
|
|
return
|
|
end
|
|
|
|
on_line_impl(self, buf, line, false)
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
|
|
local self = TSHighlighter.active[buf]
|
|
if not self then
|
|
return
|
|
end
|
|
|
|
self:reset_highlight_state()
|
|
|
|
for row = srow, erow do
|
|
on_line_impl(self, buf, row, true)
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter._on_buf(_, buf)
|
|
local self = TSHighlighter.active[buf]
|
|
if self then
|
|
self.tree:parse()
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter._on_win(_, _win, buf, _topline)
|
|
local self = TSHighlighter.active[buf]
|
|
if not self then
|
|
return false
|
|
end
|
|
|
|
self:reset_highlight_state()
|
|
self.redraw_count = self.redraw_count + 1
|
|
return true
|
|
end
|
|
|
|
a.nvim_set_decoration_provider(ns, {
|
|
on_buf = TSHighlighter._on_buf,
|
|
on_win = TSHighlighter._on_win,
|
|
on_line = TSHighlighter._on_line,
|
|
_on_spell_nav = TSHighlighter._on_spell_nav,
|
|
})
|
|
|
|
return TSHighlighter
|