kevinhwang91 / nvim-ufo

Not UFO in the sky, but an ultra fold in Neovim.
BSD 3-Clause "New" or "Revised" License
2.3k stars 47 forks source link

Is it possible to use both LSP and custom fold expression? #125

Closed D00mch closed 1 year ago

D00mch commented 1 year ago

Feature description

I want to use LSP foldings and also have the ability to add custom foldings.

Example: I have a clojure file. Everything is folded with LSP except comments: image

I can fold comments with: :set foldmethod=expr foldexpr=getline(v:lnum)=~'^\\s*'.&commentstring[0]

But I then I lost LSP foldings.

Describe the solution you'd like

Maybe a config option like "additional_fold_expr"?

Additional context

I tried to achieve this with "provider_selector", but wasn't able.

But when I wrote: :set foldmethod=expr foldexpr=getline(v:lnum)=~'^\\s*'.&commentstring[0] And relaunch my session (using session_manager), both LSP and custom foldings worked. But it's accidental (session manager shortes almost everything).

So I know it's possible to achieve, but don't know how to do it consistently.

kevinhwang91 commented 1 year ago

No, you need to wrap the result return from LSP provider. https://github.com/kevinhwang91/nvim-ufo/blob/9e829d5cfa3de6a2ff561d86399772b0339ae49d/doc/example.lua#L36-L56

Keyword: provider_selector and getFolds

D00mch commented 1 year ago

Sory if it's a stupid question, but how is this :set foldmethod=expr foldexpr=getline(v:lnum)=~'^\\s*'.&commentstring[0] suppose to work?

As I get from the snippets you provided, we fallback to using treesitter, and next — indent. But both of them have nothing to do with the foldmethod=expr. And as I get it, there is no way to return 'expr' from the provider_selector.

kevinhwang91 commented 1 year ago

https://github.com/kevinhwang91/nvim-ufo/blob/9e829d5cfa3de6a2ff561d86399772b0339ae49d/doc/example.lua#L17

Use a function to get the LSP result via getFolds API and inspect the result, you also need to scan/prase the buffer content and inject the folds to the raw result and then return.

local ftMap = {
    clojure = function (bufnr)
        return require('ufo').getFolds(bufnr, 'lsp'):thenCall(function (raw)
            print(vim.inspect(raw))
            return raw
        end)
    end
}

require('ufo').setup({
    provider_selector = function(bufnr, filetype, buftype)
        -- return ftMap[filetype] or {'treesitter', 'indent'}
        return ftMap[filetype]
    end
})
D00mch commented 1 year ago

I am almost there. But the maestro's touch is still required. I have a function that returns ranges:

local function get_comment_folds(bufnr)
    local comment_folds = {}
    local line_count = vim.api.nvim_buf_line_count(bufnr)
    local is_in_comment = false
    local comment_start = 0

    for i = 0, line_count - 1 do
        local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1]
        if not is_in_comment and line:match('^%s*' .. vim.o.commentstring:sub(1, 1)) then
            is_in_comment = true
            comment_start = i
        elseif is_in_comment and not line:match('^%s*' .. vim.o.commentstring:sub(1, 1)) then
            is_in_comment = false
            table.insert(comment_folds, {startLine = comment_start, endLine = i - 1})
        end
    end

    if is_in_comment then
        table.insert(comment_folds, {startLine = comment_start, endLine = line_count - 1})
    end

    return comment_folds
end

Here is an example of what it returns:

{{endLine = 8, startLine = 8},
 {endLine = 16, startLine = 16},
 {endLine = 47, startLine = 47},
 {endLine = 101, startLine = 98},
 {endLine = 102, startLine = 103}} 

I managed to make it work with treesitter this way:

local function treesitter_and_comment_folding(bufnr)
  local comment_folds = get_comment_folds(bufnr)
  local lsp_folds = ufo.getFolds(bufnr, "treesitter")
  for _, fold in ipairs(comment_folds) do
    table.insert(lsp_folds, fold)
  end
  print(vim.inspect(lsp_folds))
  return lsp_folds
end

So something like this should work:

local function lsp_and_comment_folding(bufnr)
    local comment_folds = get_comment_folds(bufnr)

    return ufo.getFolds(bufnr, 'lsp'):thenCall(function(lsp_folds)
        for _, fold in ipairs(comment_folds) do
            table.insert(lsp_folds, fold)
        end
        return lsp_folds
    end)
end

But it doesn't.

I wasn't able to print anything except nil with this code:

local ftMap = {
    clojure = function (bufnr)
        return require('ufo').getFolds(bufnr, 'lsp'):thenCall(function (raw)
            print(vim.inspect(raw))
            return raw
        end)
    end
}
D00mch commented 1 year ago

I use fennel to config neovim, it allows me to evaluate expressions in place. Here is an example of what ufo.getFolds(bufnr, 'treesitter') and ufo.getFolds(bufnr, 'lsp') returns accordingly for a buffer with Clojure code:

(ufo.getFolds 12 :treesitter) => [{:endLine 4 :startLine 0}
                                  {:endLine 10 :startLine 9}
                                  {:endLine 26 :startLine 12}
                                  {:endLine 33 :startLine 28}]

(ufo.getFolds 12 :lsp) => {:queue {} :state 1}

The last one also produces an exception:

Error executing vim.schedule lua callback: UnhandledPromiseRejection with the reason:
...pack/packer/start/nvim-ufo/lua/ufo/provider/lsp/nvim.lua:75: UfoFallbackException

LSP was attached to the buffer 12 at the moment, and all the lsp foldings work (except comments one).

kevinhwang91 commented 1 year ago

I don't use fennel and clojure. I guess your provider is treesitter instead of lsp.

Run UfoInspect in the target buffer with default provider_selector.

D00mch commented 1 year ago

I run it in Dart:

Buffer: 20
Fold Status: start
Main provider: lsp
Fallback provider: indent
Selected provider: lsp
Fold kinds: comment, imports

And in Clojure:

Buffer: 10
Fold Status: pending
Main provider: lsp
Fallback provider: indent
Selected provider: indent
Fold kinds:

For both of them (ufo.getFolds 5 :lsp) returns {queue={}, state=1}

I guess your provider is treesitter instead of lsp.

I am sure it's LSP, because when I change provider_selector to use treesitter or indent, I can see that foldings are different.

kevinhwang91 commented 1 year ago

Selected provider: indent

Saying that no LSP provider and use indent as fallback.

If you wan to extend treesitter provider, you can set extra fold query directly without setuping provider_selector.

D00mch commented 1 year ago

I've successfully managed to get the desired folding functionality working. Here are a few notes for those who may encounter similar issues.

By default, LSP provides foldable comment blocks for some languages I've tested (typescript, dart). However, Clojure wasn't one of them. To address this, I implemented indent folding along with custom comment folding for Clojure (like in the treesitter_and_comment_folding example above).

kevinhwang91 commented 1 year ago

Great to hear this.