nvim-ufo icon indicating copy to clipboard operation
nvim-ufo copied to clipboard

feat(treesitter): capture node range metadata as well

Open wizardlink opened this issue 9 months ago • 4 comments

Hi there! I hope this isn't too drastic of a change.

Recently venturing into the beautiful yet frustrating world of tree-sitter I have found myself in a corner where I wanted to fold region directives in C#, i.e.:

        #region Search
        Context.SearchFields = new Dictionary<string, string>()
        {
            { nameof(Response.Name), "Name" },
            { nameof(Response.Email), "Email" },
            { nameof(Response.DateOfBirth), "Date of Birth" },
            { nameof(Response.CountryID), "Country" },
            { nameof(Response.Address), "Address" },
        };

        var people = _peopleService.GetFiltered(searchBy, searchString);
        #endregion

I then found out about make-range which seems to be supported but not at the same time? Either way, I started using it but found another pitfall: The named capture for these directives encapsulate the next line as well. Finally I learned about out about the offset directive, which adds a range metadata to the captured node.

And here is where we're at - this PR which allows for the usage of the range metadata for nodes used in the make-range directive. I've tested with and without the metadata and it behaved normally, so I don't think it would break anything, please let me know otherwise!

There is one caveat with this though, the other place that calls for from_nodes I have left unchanged:

https://github.com/kevinhwang91/nvim-ufo/blob/81f5ffa6e8ba27c48403cf681d4b383d924e03e4/lua/ufo/provider/treesitter.lua?plain=1#L112

It theory it would benefit from this too and then line 111 would be unnecessary, however offsets cannot be applied to multiple nodes, which is a bit unfortunate.

wizardlink avatar Feb 13 '25 00:02 wizardlink

Please show the difference between the examples before and after the patch.

kevinhwang91 avatar Feb 13 '25 05:02 kevinhwang91

Using the following fold query for c_sharp:

(
  (preproc_region) @region_begin
  .
  [
    (comment)
    (declaration)
    (statement)
    (type_declaration)
  ]*
  .
  (preproc_endregion) @region_end (#offset! @region_end 0 0 -1 0)
  (#make-range! "fold" @region_begin @region_end)
)

When nvim-ufo collects the folds in the snippet mentioned previously it folds the following:

#region Search // FOLD START
Context.SearchFields = new Dictionary<string, string>()
{
    { nameof(Response.Name), "Name" },
    { nameof(Response.Email), "Email" },
    { nameof(Response.DateOfBirth), "Date of Birth" },
    { nameof(Response.CountryID), "Country" },
    { nameof(Response.Address), "Address" },
};

var people = _peopleService.GetFiltered(searchBy, searchString);
#endregion
// FOLD END

With the adjustments of the PR it respects the offset directive changes:

#region Search // FOLD START
Context.SearchFields = new Dictionary<string, string>()
{
    { nameof(Response.Name), "Name" },
    { nameof(Response.Email), "Email" },
    { nameof(Response.DateOfBirth), "Date of Birth" },
    { nameof(Response.CountryID), "Country" },
    { nameof(Response.Address), "Address" },
};

var people = _peopleService.GetFiltered(searchBy, searchString);
#endregion // FOLD END

wizardlink avatar Feb 13 '25 16:02 wizardlink

It looks like nothing happened after the patch. query:

body: [
  (declaration_list)
  (switch_body)
  (enum_member_declaration_list)
] @fold

accessors: (accessor_list) @fold

initializer: (initializer_expression) @fold

[
  (block)
  (preproc_if)
  (preproc_elif)
  (preproc_else)
  (using_directive)+
] @fold

(
  (preproc_region) @region_begin
  .
  [
    (comment)
    (declaration)
    (statement)
    (type_declaration)
  ]*
  .
  (preproc_endregion) @region_end (#offset! @region_end 0 0 -1 0)
  (#make-range! "fold" @region_begin @region_end)
)

demo:

class Test
{
    #region Search
    Context.SearchFields = new Dictionary<string, string>()
    {
        { nameof(Response.Name), "Name" },
        { nameof(Response.Email), "Email" },
        { nameof(Response.DateOfBirth), "Date of Birth" },
        { nameof(Response.CountryID), "Country" },
        { nameof(Response.Address), "Address" },
    };

    var people = _peopleService.GetFiltered(searchBy, searchString);
    #endregion
}

image

kevinhwang91 avatar Feb 14 '25 09:02 kevinhwang91

Add any code after that region and you will see that the fold extends one line more, i.e.

class Test
{
    #region Search
    Context.SearchFields = new Dictionary<string, string>()
    {
        { nameof(Response.Name), "Name" },
        { nameof(Response.Email), "Email" },
        { nameof(Response.DateOfBirth), "Date of Birth" },
        { nameof(Response.CountryID), "Country" },
        { nameof(Response.Address), "Address" },
    };

    var people = _peopleService.GetFiltered(searchBy, searchString);
    #endregion

    public string SomeString { get; set; }
}

Without the offset it will wrap the empty line below it: image

Then with the offset: image

wizardlink avatar Feb 16 '25 23:02 wizardlink