From 416d75fb824df8494df681e3a63996ebf6c5c565 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Tue, 24 Sep 2024 08:41:46 -0700 Subject: [PATCH] feat(comment): allow commentstring to be determined from node metadata **Problem:** Some weird languages have different comment syntax depending on the location in the code, and we do not have a way to determine the correct `commentstring` for these special cases. **Solution:** Allow queries to specify `commentstring` values in metadata, allowing users/`nvim-treesitter` to provide a better commenting experience without hugely increasing the scope of the code in core. --- runtime/doc/news.txt | 4 + runtime/doc/treesitter.txt | 13 +++ runtime/lua/vim/_comment.lua | 12 +++ runtime/lua/vim/treesitter/query.lua | 1 + test/functional/lua/comment_spec.lua | 134 +++++++++++++++++++++++++++ 5 files changed, 164 insertions(+) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 07b1b8646a..1d556a7fcc 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -264,6 +264,10 @@ PLUGINS • EditorConfig • spelling_language property is now supported. +• Commenting + • 'commentstring' values can now be specified in a Treesitter capture's + `bo.commentstring` metadata field, providing finer grained support for + languages like `JSX`. STARTUP diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index a0860c60a6..46e15ecdf1 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -519,6 +519,19 @@ attribute: >query ((super_important_node) @superimportant (#set! priority 105)) < + *treesitter-highlight-commentstring* +Treesitter highlighting supports finer-grained 'commentstring's, used by the +built-in |commenting| plugin. When the cursor is within a node that sets the +`bo.commentstring` metadata property (|treesitter-directive-set!|), that +property defines the comment delimiter (where "innermost wins"). This is +useful for languages like `JSX` that have different comment syntax depending +on the code region, for example: >query + + ((jsx_element) @_tag (#set! @_tag bo.commentstring "{/* %s */}")) +< +When multiple captures set this metadata over a region, only the innermost +(most specific) one is applied to a given area. + ============================================================================== TREESITTER LANGUAGE INJECTIONS *treesitter-language-injections* < diff --git a/runtime/lua/vim/_comment.lua b/runtime/lua/vim/_comment.lua index de7f62632c..57fd3d73d6 100644 --- a/runtime/lua/vim/_comment.lua +++ b/runtime/lua/vim/_comment.lua @@ -19,6 +19,18 @@ local function get_commentstring(ref_position) local row, col = ref_position[1] - 1, ref_position[2] local ref_range = { row, col, row, col + 1 } + -- Get 'commentstring' from tree-sitter captures' metadata. + -- Traverse backwards to prefer narrower captures. + local caps = vim.treesitter.get_captures_at_pos(0, row, col) + for i = #caps, 1, -1 do + local id, metadata = caps[i].id, caps[i].metadata + local md_cms = metadata['bo.commentstring'] or metadata[id] and metadata[id]['bo.commentstring'] + + if md_cms then + return md_cms + end + end + -- - Get 'commentstring' from the deepest LanguageTree which both contains -- reference range and has valid 'commentstring' (meaning it has at least -- one associated 'filetype' with valid 'commentstring'). diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index dbe3d54c2f..a67e93f633 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -486,6 +486,7 @@ predicate_handlers['any-vim-match?'] = predicate_handlers['any-match?'] ---@class vim.treesitter.query.TSMetadata ---@field range? Range ---@field conceal? string +---@field bo.commentstring? string ---@field [integer]? vim.treesitter.query.TSMetadata ---@field [string]? integer|string diff --git a/test/functional/lua/comment_spec.lua b/test/functional/lua/comment_spec.lua index bbf061a2ab..9ae9ce84d0 100644 --- a/test/functional/lua/comment_spec.lua +++ b/test/functional/lua/comment_spec.lua @@ -586,6 +586,140 @@ describe('commenting', function() feed('.') eq(get_lines(), { '"set background=dark', 'lua << EOF', '-- print(1)', 'EOF' }) end) + + it('respects tree-sitter commentstring metadata', function() + exec_lua [=[ + vim.treesitter.query.set('vim', 'highlights', [[ + ((list) @_list (#set! @_list bo.commentstring "!! %s")) + ]]) + ]=] + setup_treesitter() + + local lines = { + 'set background=dark', + 'let mylist = [', + [[ \"a",]], + [[ \"b",]], + [[ \"c",]], + ' \\]', + } + set_lines(lines) + + set_cursor(1, 0) + feed('gcc') + eq( + { '"set background=dark', 'let mylist = [', [[ \"a",]], [[ \"b",]], [[ \"c",]], ' \\]' }, + get_lines() + ) + + -- Should work with dot-repeat + set_cursor(4, 0) + feed('.') + eq({ + '"set background=dark', + 'let mylist = [', + [[ \"a",]], + [[ !! \"b",]], + [[ \"c",]], + ' \\]', + }, get_lines()) + end) + + it('only applies the innermost tree-sitter commentstring metadata', function() + exec_lua [=[ + vim.treesitter.query.set('vim', 'highlights', [[ + ((list) @_list (#gsub! @_list "(.*)" "%1") (#set! bo.commentstring "!! %s")) + ((script_file) @_src (#set! @_src bo.commentstring "## %s")) + ]]) + ]=] + setup_treesitter() + + local lines = { + 'set background=dark', + 'let mylist = [', + [[ \"a",]], + [[ \"b",]], + [[ \"c",]], + ' \\]', + } + set_lines(lines) + + set_cursor(1, 0) + feed('gcc') + eq({ + '## set background=dark', + 'let mylist = [', + [[ \"a",]], + [[ \"b",]], + [[ \"c",]], + ' \\]', + }, get_lines()) + + -- Should work with dot-repeat + set_cursor(4, 0) + feed('.') + eq({ + '## set background=dark', + 'let mylist = [', + [[ \"a",]], + [[ !! \"b",]], + [[ \"c",]], + ' \\]', + }, get_lines()) + end) + + it('respects injected tree-sitter commentstring metadata', function() + exec_lua [=[ + vim.treesitter.query.set('lua', 'highlights', [[ + ((string) @string (#set! @string bo.commentstring "; %s")) + ]]) + ]=] + setup_treesitter() + + local lines = { + 'set background=dark', + 'lua << EOF', + 'print[[', + 'Inside string', + ']]', + 'EOF', + } + set_lines(lines) + + set_cursor(1, 0) + feed('gcc') + eq({ + '"set background=dark', + 'lua << EOF', + 'print[[', + 'Inside string', + ']]', + 'EOF', + }, get_lines()) + + -- Should work with dot-repeat + set_cursor(4, 0) + feed('.') + eq({ + '"set background=dark', + 'lua << EOF', + 'print[[', + '; Inside string', + ']]', + 'EOF', + }, get_lines()) + + set_cursor(3, 0) + feed('.') + eq({ + '"set background=dark', + 'lua << EOF', + '-- print[[', + '; Inside string', + ']]', + 'EOF', + }, get_lines()) + end) end) describe('Textobject', function()