From c1e8102e4e954e1c46f46cda2587769238e24f16 Mon Sep 17 00:00:00 2001 From: Jongwook Choi Date: Tue, 19 Mar 2024 11:53:46 -0400 Subject: [PATCH] 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. --- README.md | 5 ++-- lua/ufo/provider/treesitter.lua | 49 ++++++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0cb57c0..1fff7f1 100644 --- a/README.md +++ b/README.md @@ -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//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'} diff --git a/lua/ufo/provider/treesitter.lua b/lua/ufo/provider/treesitter.lua index 85974fc..ae9094c 100644 --- a/lua/ufo/provider/treesitter.lua +++ b/lua/ufo/provider/treesitter.lua @@ -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')