refactor(treesitter): remove nvim-treesitter dependencies (#189)

Replace all the use of nvim-treesitter APIs with core vim.treesitter
APIs. No more nvim-treesitter dependency, just core neovim is enough.

This fix makes ufo work without the nvim-treesitter plugin as an
additional dependency. In fact, nvim-treesitter v1.0 deprecates and
removes some APIs that have been migrated to the core neovim APIs
`vim.treesitter`, which makes ufo's previous treesitter provider
implementation incompatible.

Note that this commit does not change the minimum neovim version
requirement, should work fine with neovim 0.7.x.

Implementation note:

There are four APIs that need to be migrated:

- `nvim-treesitter.parsers.get_parser()`: The difference to the core API
  `vim.treesitter.get_parser()` is whether to throw errors when a parser
  is not available (`has_parser`). We simply mimic the previous behavior
  by catching errors.

- `nvim-treesitter.query.get_query()`: The difference to core API
  `vim.treesitter.query.get()` is whether the query file is cached or
  not. This may have a small performance impact; in neovim 0.10.x, this
  function is memoized and thus very fast, but in neovim <= 0.9.x it
  might be slightly slow due to the lack of cache.

  Note: One can consider as well automatically falling back to the old
  nvim-treesitter (v0.9.x) if available, for neovim < 0.10.

- `nvim-treesitter.query.has_folds()` (i.e., `has_query_files()`):
  can be easily replaced with `vim.treesitter.query.get_files`. Also
  there might be a subtle performance difference of whether cache is
  being used (in the old nvim-treesitter implementations) or not.

- `nvim-treesitter.tsrange`: The `TSRange` API has gone. Note that this
  is used only to implement the `#make-range!` directive; it suffices to
  have `node:range()` only for where it's used. Therefore,
  `TSRange.from_nodes()` is the only API we'll need, which can be easily
  backported into the existing `MetaNode` implementation.
This commit is contained in:
Jongwook Choi 2024-03-19 11:53:46 -04:00 committed by GitHub
parent dd83ca6f8f
commit c1e8102e4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 10 deletions

View file

@ -44,7 +44,6 @@ The goal of nvim-ufo is to make Neovim's fold look modern and keep high performa
- [Neovim](https://github.com/neovim/neovim) 0.7.2 or later
- [coc.nvim](https://github.com/neoclide/coc.nvim) (optional)
- [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) (optional)
### Installation
@ -92,9 +91,9 @@ require('ufo').setup()
--
-- Option 3: treesitter as a main provider instead
-- Only depend on `nvim-treesitter/queries/filetype/folds.scm`,
-- (Note: the `nvim-treesitter` plugin is *not* needed.)
-- ufo uses the same query files for folding (queries/<lang>/folds.scm)
-- performance and stability are better than `foldmethod=nvim_treesitter#foldexpr()`
use {'nvim-treesitter/nvim-treesitter', run = ':TSUpdate'}
require('ufo').setup({
provider_selector = function(bufnr, filetype, buftype)
return {'treesitter', 'indent'}

View file

@ -1,6 +1,3 @@
local parsers = require('nvim-treesitter.parsers')
local query = require('nvim-treesitter.query')
local tsrange = require('nvim-treesitter.tsrange')
local bufmanager = require('ufo.bufmanager')
local foldingrange = require('ufo.model.foldingrange')
@ -10,6 +7,26 @@ local Treesitter = {
hasProviders = {}
}
---@diagnostic disable: deprecated
---@return vim.treesitter.LanguageTree|nil parser for the buffer, or nil if parser is not available
local function getParser(bufnr, lang)
local ok, parser = pcall(vim.treesitter.get_parser, bufnr, lang)
if not ok then
return nil
end
return parser
end
local get_query = assert(vim.treesitter.query.get or vim.treesitter.query.get_query)
local get_query_files = assert(vim.treesitter.query.get_files or vim.treesitter.query.get_query_files)
---@diagnostic enable: deprecated
-- Backward compatibility for the dummy directive (#make-range!),
-- which no longer exists in nvim-treesitter v1.0+
if not vim.tbl_contains(vim.treesitter.query.list_directives(), "make-range!") then
vim.treesitter.query.add_directive("make-range!", function() end, {})
end
local MetaNode = {}
MetaNode.__index = MetaNode
@ -24,6 +41,19 @@ function MetaNode:range()
return range[1], range[2], range[3], range[4]
end
--- Return a meta node that represents a range between two nodes, i.e., (#make-range!),
--- that is similar to the legacy TSRange.from_node() from nvim-treesitter.
function MetaNode.from_nodes(start_node, end_node)
local start_pos = { start_node:start() }
local end_pos = { end_node:end_() }
return MetaNode:new({
[1] = start_pos[1],
[2] = start_pos[2],
[3] = end_pos[1],
[4] = end_pos[2],
})
end
local function prepareQuery(bufnr, parser, root, rootLang, queryName)
if not root then
local firstTree = parser:trees()[1]
@ -45,7 +75,7 @@ local function prepareQuery(bufnr, parser, root, rootLang, queryName)
end
end
return query.get_query(rootLang, queryName), {
return get_query(rootLang, queryName), {
root = root,
source = bufnr,
start = range[1],
@ -67,6 +97,8 @@ local function iterFoldMatches(bufnr, parser, root, rootLang)
if pattern == nil then
return pattern
end
-- Extract capture names from each match
for id, node in pairs(match) do
local m = metadata[id]
if m and m.range then
@ -74,11 +106,13 @@ local function iterFoldMatches(bufnr, parser, root, rootLang)
end
table.insert(matches, node)
end
-- Add some predicates for testing
local preds = q.info.patterns[pattern]
if preds then
for _, pred in pairs(preds) do
if pred[1] == 'make-range!' and type(pred[2]) == 'string' and #pred == 4 then
local node = tsrange.TSRange.from_nodes(bufnr, match[pred[3]], match[pred[4]])
local node = MetaNode.from_nodes(match[pred[3]], match[pred[4]])
table.insert(matches, node)
end
end
@ -101,7 +135,8 @@ local function getCpatureMatchesRecursively(bufnr, parser)
local res = {}
parser:for_each_tree(function(tree, langTree)
local lang = langTree:lang()
if query.has_folds(lang) then
local has_folds = #get_query_files(lang, 'folds', nil) > 0
if has_folds then
noQuery = false
getFoldMatches(res, bufnr, parser, tree:root(), lang)
end
@ -126,7 +161,7 @@ function Treesitter.getFolds(bufnr)
if self.hasProviders[ft] == false then
error('UfoFallbackException')
end
local parser = parsers.get_parser(bufnr)
local parser = getParser(bufnr)
if not parser then
self.hasProviders[ft] = false
error('UfoFallbackException')