├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ └── default.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc └── nvim-ts-context-commentstring.txt ├── lua ├── ts_context_commentstring.lua └── ts_context_commentstring │ ├── config.lua │ ├── integrations │ ├── comment_nvim.lua │ └── vim_commentary.lua │ ├── internal.lua │ └── utils.lua ├── plugin └── ts_context_commentstring.lua ├── stylua.toml └── utils ├── minimal_init.lua └── run_minimal.sh /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a problem in nvim-ts-context-commentstring 3 | labels: [bug] 4 | 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: "Minimal reproducible full config" 9 | description: | 10 | Please provide a working config based on [this](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/blob/main/utils/minimal_init.lua). 11 | 12 | 1. Clone this plugin somewhere. E.g. `git clone https://github.com/JoosepAlviste/nvim-ts-context-commentstring ~/nvim-repro/nvim-ts-context-commentstring` 13 | 2. Run `utils/run_minimal.sh` from the cloned repository 14 | 3. Make the necessary changes to the config file to reproduce your issue 15 | value: | 16 | ```lua 17 | -- Your configuration here 18 | ``` 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: "Description" 25 | description: "Describe in detail what happens" 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: "Steps to reproduce" 32 | description: "Full reproduction steps. Please include a sample file if your issue relates to a specific filetype." 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: "Expected behavior" 39 | description: "A description of the behavior you expected." 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: "Actual behavior" 46 | description: "A description of the actual behavior." 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | attributes: 52 | label: "Additional context" 53 | description: "Any other relevant information" 54 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: default 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Lint Lua code 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - name: Check out repo 11 | uses: actions/checkout@v2 12 | - name: Stylua check 13 | uses: JohnnyMorganz/stylua-action@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | args: --color always --check . 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | utils/.testenv 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joosep Alviste 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `nvim-ts-context-commentstring` 2 | 3 | A Neovim plugin for setting the `commentstring` option based on the cursor 4 | location in the file. The location is checked via treesitter queries. 5 | 6 | This is useful when there are embedded languages in certain types of files. For 7 | example, Vue files can have many different sections, each of which can have a 8 | different style for comments. 9 | 10 | Note that this plugin *only* changes the `commentstring` setting. It does not 11 | add any mappings for commenting. It is recommended to use a commenting plugin 12 | like [`Comment.nvim`](https://github.com/numToStr/Comment.nvim) alongside this 13 | plugin. 14 | 15 | ![Demo gif](https://user-images.githubusercontent.com/9450943/185669080-a5f05064-c247-47f5-9b63-d34a9871186e.gif) 16 | 17 | 18 | 19 | ## Getting started 20 | 21 | **Requirements:** 22 | 23 | - [Neovim version 0.9.4](https://github.com/neovim/neovim/releases/tag/v0.9.4) 24 | - Tree-sitter parsers (e.g. installed with [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)) 25 | 26 | **Installation:** 27 | 28 | Use your favorite plugin manager. For example, here's how it would look like 29 | with [lazy.nvim](https://github.com/folke/lazy.nvim): 30 | 31 | ```lua 32 | require('lazy').setup { 33 | 'JoosepAlviste/nvim-ts-context-commentstring', 34 | } 35 | ``` 36 | 37 | **Setup:** 38 | 39 | For most commenting plugins, this is all you need and the defaults should work 40 | out of the box. However, some commenting plugins require a bit more set up. See 41 | the [Configuration section](#configuration) for more details about the different 42 | plugins. 43 | 44 | If you want to change the configuration, call the `setup` function of this 45 | plugin, e.g.: 46 | 47 | ```lua 48 | require('ts_context_commentstring').setup { 49 | enable_autocmd = false, 50 | } 51 | ``` 52 | 53 | > **Note** 54 | > 55 | > There is a minimal configuration file available at 56 | > [`utils/minimal_init.lua`](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/blob/main/utils/minimal_init.lua) for reference. 57 | 58 | > **Note** 59 | > 60 | > Don't forget to use `:h lua-heredoc` if you're using `init.vim`. 61 | 62 | 63 | ## Configuration 64 | 65 | It is recommended to use a commenting plugin that has an integration available 66 | with this plugin. Then, the `commentstring` calculation can be triggered only 67 | when commenting. The available integrations are listed in the 68 | [wiki](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations). 69 | The following plugins have an integration available: 70 | 71 | - [Native Neovim commenting](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations#native-commenting-in-neovim-010) 72 | - [`b3nj5m1n/kommentary`](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations#kommentary) 73 | - [`terrortylor/nvim-comment`](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations#nvim-comment) 74 | - [`numToStr/Comment.nvim`](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations#commentnvim) 75 | - [`echasnovski/mini.nvim/mini-comment`](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations#minicomment) 76 | - [`tpope/vim-commentary`](https://github.com/JoosepAlviste/nvim-ts-context-commentstring/wiki/Integrations#vim-commentary) 77 | 78 | However, if an integration is not set up, then the default behavior is to 79 | calculate the `commentstring` on the `CursorHold` autocmd, meaning that the 80 | `:h updatetime` should be set to a smaller value than the default of 4s: 81 | 82 | ```lua 83 | vim.opt.updatetime = 100 84 | ``` 85 | 86 | > **Note** 87 | > 88 | > For more advanced configuration options, see `:h ts-context-commentstring`. 89 | 90 | 91 | ## More demos 92 | 93 | **React:** 94 | 95 | ![React demo gif](https://user-images.githubusercontent.com/9450943/185669182-d523c328-251e-41b0-a76e-d867c401a040.gif) 96 | 97 | **Svelte:** 98 | 99 | ![Svelte demo gif](https://user-images.githubusercontent.com/9450943/185669229-ad10848e-ba13-45e0-8447-a3a1f03eb85e.gif) 100 | 101 | **HTML:** 102 | 103 | ![html](https://user-images.githubusercontent.com/9450943/185669275-cdfa7fa4-092e-439b-822e-330559a7d4d7.gif) 104 | 105 | **Nesting:** 106 | 107 | I injected HTML into JavaScript strings and created multiple levels of nesting 108 | with language tree. This sort of nesting of languages works without any extra 109 | configuration in the plugin. 110 | 111 | ![nested](https://user-images.githubusercontent.com/9450943/185669303-e6958706-f5b7-439c-98f7-2393e6325107.gif) 112 | -------------------------------------------------------------------------------- /doc/nvim-ts-context-commentstring.txt: -------------------------------------------------------------------------------- 1 | *ts-context-commentstring.txt* 2 | 3 | ============================================================================== 4 | 1. nvim-ts-context-commentstring *ts-context-commentstring-intro* 5 | 6 | *ts-context-commentstring* is a Neovim plugin that uses Treesitter to set the 7 | 'commentstring' option based on the cursor location in a file. 8 | 9 | Please see the basic configuration in the `README.md` file. This file only 10 | includes more advanced documentation. 11 | 12 | ============================================================================== 13 | 2. Supported languages *ts-context-commentstring-languages* 14 | 15 | Currently, the following languages are supported when they are injected with 16 | language tree (see `lua/ts_context_commentstring/internal.lua`): 17 | 18 | - `astro` 19 | - `bash` 20 | - `c` 21 | - `cpp` 22 | - `css` 23 | - `cue` 24 | - `gleam` 25 | - `glimmer` 26 | - `go` 27 | - `graphql` 28 | - `handlebars` 29 | - `haskell` 30 | - `hcl` 31 | - `html` 32 | - `htmldjango` 33 | - `ini` 34 | - `javascript` 35 | - `kotlin` 36 | - `lua` 37 | - `nix` 38 | - `php` 39 | - `python` 40 | - `rego` 41 | - `rescript` 42 | - `roc` 43 | - `scss` 44 | - `shell` 45 | - `solidity` 46 | - `sql` 47 | - `svelte` 48 | - `terraform` 49 | - `tsx` 50 | - `twig` 51 | - `typescript` 52 | - `typst` 53 | - `vim` 54 | - `vue` 55 | - `zsh` 56 | 57 | This means that in any filetype, if the given languages are injected, this 58 | plugin should detect them and correctly set the 'commentstring'. For example, 59 | Vue files can be injected with `css` or `javascript`. Even though we don't 60 | configure anything for Vue explicitly, the 'commentstring' updating logic 61 | should still work. 62 | 63 | 64 | ============================================================================== 65 | 3. Commentstring configuration *ts-context-commentstring-commentstring-configuration* 66 | 67 | This plugin initializes using default **plugin** system. 68 | 69 | If you need to disable auto-initialization, set 70 | `g:loaded_ts_context_commentstring` to non-zero value. 71 | 72 | Custom configuration can be supplied using `setup` function: 73 | >lua 74 | require('ts_context_commentstring').setup { 75 | -- ... configuration here 76 | } 77 | < 78 | 79 | Support for more languages can be added quite easily by passing a `languages` (formerly the now deprecated `config`) table 80 | when configuring the plugin: 81 | >lua 82 | require('ts_context_commentstring').setup { 83 | languages = { 84 | css = '// %s', 85 | }, 86 | } 87 | < 88 | 89 | Additionally, some languages are not injected with language tree, but have 90 | multiple commenting styles in the same language. One such example is 91 | JavaScript with JSX. The JSX section is not an injected language, but a part 92 | of the tree generated by the `javascript` Treesitter parser. 93 | 94 | In this more complex case, this plugin supports adding queries for specific 95 | Treesitter nodes. Each node can have its own unique commenting style. For 96 | example, here's how the default configuration for `javascript` would look 97 | like: 98 | >lua 99 | require('ts_context_commentstring').setup { 100 | languages = { 101 | javascript = { 102 | __default = '// %s', 103 | jsx_element = '{/* %s */}', 104 | jsx_fragment = '{/* %s */}', 105 | jsx_attribute = '// %s', 106 | comment = '// %s', 107 | }, 108 | }, 109 | } 110 | < 111 | 112 | The `__default` value is used when none of the other node types are seen. The 113 | rest of the keys refer to the type of the Treesitter node. In this example, if 114 | your cursor is inside a `jsx_element`, then the `{/* %s */}` 'commentstring' 115 | will be set. 116 | 117 | Note that the language refers to the |treesitter| language, not the filetype 118 | or the file extension. 119 | 120 | Additionally, it is possible to have each 'commentstring' configuration be a 121 | table with custom keys. This can be used to configure separate single and 122 | multi-line comment styles (useful when integrating with a commenting plugin): 123 | >lua 124 | require('ts_context_commentstring').setup { 125 | languages = { 126 | typescript = { __default = '// %s', __multiline = '/* %s */' }, 127 | }, 128 | } 129 | < 130 | 131 | Then, the custom key can be passed to `update_commentstring`: 132 | >lua 133 | require('ts_context_commentstring').update_commentstring { 134 | key = '__multiline', 135 | } 136 | < 137 | 138 | Finally, it is possible to customize the tree traversal start location when 139 | calling `update_commentstring`, this is useful in commenting plugin 140 | integrations. There are some useful helper functions exported from 141 | `ts_context_commentstring.utils`: 142 | >lua 143 | require('ts_context_commentstring').calculate_commentstring { 144 | location = require('ts_context_commentstring.utils').get_cursor_location(), 145 | } 146 | < 147 | 148 | If you want to calculate your own 'commentstring' you are able to do so with 149 | the `custom_calculation` option: 150 | >lua 151 | require('ts_context_commentstring').setup { 152 | custom_calculation = function(node, language_tree) 153 | -- ... 154 | end, 155 | } 156 | < 157 | 158 | This is a function that takes in the current node and the language tree which 159 | could be used for context like figuring out which language you should use a 160 | 'commentstring' for. You can also for example figure out which type the current 161 | node is. You need to return a 'commentstring' in the `custom_calculation` if you 162 | want it to be set. 163 | 164 | The `not_nested_languages` table allows you to halt the search for nested 165 | languages at a specified language. This functionality proves beneficial, 166 | for instance, in scenarios such as HTML with Django, where you may want 167 | to include Django comments while excluding HTML content. 168 | 169 | ============================================================================== 170 | vim:tw=78:ts=8:ft=help:norl: 171 | -------------------------------------------------------------------------------- /lua/ts_context_commentstring.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---Set up non-default configuration 4 | ---@param config? ts_context_commentstring.Config 5 | function M.setup(config) 6 | require('ts_context_commentstring.config').update(config) 7 | end 8 | 9 | ---Calculate the commentstring based on the current location of the cursor. 10 | --- 11 | ---@param args? ts_context_commentstring.Args 12 | --- 13 | ---@return string | nil commentstring If found, otherwise `nil` 14 | function M.calculate_commentstring(args) 15 | return require('ts_context_commentstring.internal').calculate_commentstring(args) 16 | end 17 | 18 | ---Update the `commentstring` setting based on the current location of the 19 | ---cursor. If no `commentstring` can be calculated, will revert to the ofiginal 20 | ---`commentstring` for the current file. 21 | --- 22 | ---@param args? ts_context_commentstring.Args 23 | function M.update_commentstring(args) 24 | return require('ts_context_commentstring.internal').update_commentstring(args) 25 | end 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /lua/ts_context_commentstring/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---A commentstring configuration that includes both single and multi-line 4 | ---comments. The fields can be anything and they will be retrievable with the 5 | ---`key` option to `update_commentstring`. 6 | ---@class ts_context_commentstring.CommentConfigMultiple 7 | ---@field __default string Single-line commentstring 8 | ---@field __multiline string Multi-line commentstring 9 | 10 | ---Commentstring configuration can either be a string (a single commenting 11 | ---style) or a table specifying multiple styles. 12 | ---@alias ts_context_commentstring.CommentConfig string | ts_context_commentstring.CommentConfigMultiple 13 | 14 | ---The comment configuration for a language. 15 | ---@alias ts_context_commentstring.LanguageConfig ts_context_commentstring.CommentConfig | table 16 | 17 | ---Configuration of the languages to commentstring configs. 18 | --- 19 | ---The configuration object keys should be **treesitter** languages, NOT 20 | ---filetypes or file extensions. 21 | --- 22 | ---You can get the treesitter language for the current file by running this 23 | ---command: 24 | ---`:=vim.treesitter.get_parser():lang()` 25 | --- 26 | ---Or the injected language for a specific location: 27 | ---`:=vim.treesitter.get_parser():language_for_range({ line, col, line, col }):lang())` 28 | --- 29 | ---@alias ts_context_commentstring.LanguagesConfig table 30 | 31 | ---@class ts_context_commentstring.CommentaryConfig 32 | ---@field Commentary string | false | nil 33 | ---@field CommentaryLine string | false | nil 34 | ---@field ChangeCommentary string | false | nil 35 | ---@field CommentaryUndo string | false | nil 36 | 37 | ---@alias ts_context_commentstring.CustomCalculation fun(node: TSNode|nil, language_tree: vim.treesitter.LanguageTree|nil): string 38 | 39 | ---@class ts_context_commentstring.Config 40 | ---@field enable_autocmd? boolean 41 | ---@field custom_calculation? ts_context_commentstring.CustomCalculation 42 | ---@field languages? ts_context_commentstring.LanguagesConfig 43 | ---@field not_nested_languages? table 44 | ---@field config? ts_context_commentstring.LanguagesConfig 45 | ---@field commentary_integration? ts_context_commentstring.CommentaryConfig 46 | 47 | ---@type ts_context_commentstring.Config 48 | M.config = { 49 | -- Whether to update the `commentstring` on the `CursorHold` autocmd 50 | enable_autocmd = true, 51 | 52 | -- Custom logic for calculating the commentstring. 53 | custom_calculation = nil, 54 | 55 | -- Keybindings to use for the commentary.nvim integration 56 | commentary_integration = { 57 | Commentary = 'gc', 58 | CommentaryLine = 'gcc', 59 | ChangeCommentary = 'cgc', 60 | CommentaryUndo = 'gcu', 61 | }, 62 | 63 | languages = { 64 | -- Languages that have a single comment style 65 | astro = '', 66 | c = '/* %s */', 67 | cpp = { __default = '// %s', __multiline = '/* %s */' }, 68 | css = '/* %s */', 69 | cue = '// %s', 70 | gleam = '// %s', 71 | glimmer = '{{! %s }}', 72 | go = { __default = '// %s', __multiline = '/* %s */' }, 73 | graphql = '# %s', 74 | haskell = '-- %s', 75 | handlebars = '{{! %s }}', 76 | hcl = { __default = '# %s', __multiline = '/* %s */' }, 77 | html = '', 78 | htmldjango = { __default = '{# %s #}', __multiline = '{% comment %} %s {% endcomment %}' }, 79 | ini = '; %s', 80 | lua = { __default = '-- %s', __multiline = '--[[ %s ]]' }, 81 | nix = { __default = '# %s', __multiline = '/* %s */' }, 82 | php = { __default = '// %s', __multiline = '/* %s */' }, 83 | python = { __default = '# %s', __multiline = '""" %s """' }, 84 | rego = '# %s', 85 | rescript = { __default = '// %s', __multiline = '/* %s */' }, 86 | scss = { __default = '// %s', __multiline = '/* %s */' }, 87 | sh = '# %s', 88 | bash = '# %s', 89 | solidity = { __default = '// %s', __multiline = '/* %s */' }, 90 | sql = '-- %s', 91 | svelte = '', 92 | terraform = { __default = '# %s', __multiline = '/* %s */' }, 93 | twig = '{# %s #}', 94 | typescript = { __default = '// %s', __multiline = '/* %s */' }, 95 | typst = { __default = '// %s', __multiline = '/* %s */' }, 96 | vim = '" %s', 97 | vue = '', 98 | zsh = '# %s', 99 | kotlin = { __default = '// %s', __multiline = '/* %s */' }, 100 | roc = '# %s', 101 | 102 | -- Languages that can have multiple types of comments 103 | tsx = { 104 | __default = '// %s', 105 | __multiline = '/* %s */', 106 | jsx_element = '{/* %s */}', 107 | jsx_fragment = '{/* %s */}', 108 | jsx_attribute = { __default = '// %s', __multiline = '/* %s */' }, 109 | comment = { __default = '// %s', __multiline = '/* %s */' }, 110 | call_expression = { __default = '// %s', __multiline = '/* %s */' }, 111 | statement_block = { __default = '// %s', __multiline = '/* %s */' }, 112 | spread_element = { __default = '// %s', __multiline = '/* %s */' }, 113 | }, 114 | templ = { 115 | __default = '// %s', 116 | component_block = '', 117 | }, 118 | }, 119 | 120 | not_nested_languages = { 121 | -- Languages at which nesting stops 122 | htmldjango = true, 123 | }, 124 | 125 | ---@deprecated Use the languages configuration instead! 126 | config = {}, 127 | } 128 | 129 | M.config.languages.javascript = M.config.languages.tsx 130 | 131 | ---@param config? ts_context_commentstring.Config 132 | function M.update(config) 133 | M.config = vim.tbl_deep_extend('force', M.config, config or {}) 134 | end 135 | 136 | ---@return boolean 137 | function M.is_autocmd_enabled() 138 | if vim.g.loaded_commentary == 1 then 139 | return false 140 | end 141 | 142 | local enable_autocmd = M.config.enable_autocmd 143 | if enable_autocmd == nil then 144 | return true 145 | end 146 | 147 | return enable_autocmd 148 | end 149 | 150 | ---@return ts_context_commentstring.LanguagesConfig 151 | function M.get_languages_config() 152 | return vim.tbl_deep_extend('force', M.config.languages or {}, M.config.config or {}) 153 | end 154 | 155 | return M 156 | -------------------------------------------------------------------------------- /lua/ts_context_commentstring/integrations/comment_nvim.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---Use this function to get a pre_hook function that can be used when 4 | ---configuring Comment.nvim. 5 | ---https://github.com/numToStr/Comment.nvim/ 6 | --- 7 | ---Example usage: 8 | ---```lua 9 | ---require('Comment').setup { 10 | --- pre_hook = require('ts_context_commentstring.integrations.comment_nvim').create_pre_hook(), 11 | ---} 12 | ---``` 13 | --- 14 | ---Feel free to copy this function into your own configuration if you need to 15 | ---make any changes (or contribute the improvements back into this plugin). 16 | --- 17 | ---This is a higher order function in case we want to add any extra 18 | ---configuration for the hook in the future. 19 | --- 20 | ---@return fun(ctx: CommentCtx): string|nil 21 | function M.create_pre_hook() 22 | ---@param ctx CommentCtx 23 | ---@return string|nil 24 | return function(ctx) 25 | local U = require 'Comment.utils' 26 | 27 | -- Determine whether to use linewise or blockwise commentstring 28 | local type = ctx.ctype == U.ctype.linewise and '__default' or '__multiline' 29 | 30 | -- Determine the location where to calculate commentstring from 31 | local location = nil 32 | if ctx.ctype == U.ctype.blockwise then 33 | location = { 34 | ctx.range.srow - 1, 35 | ctx.range.scol, 36 | } 37 | elseif ctx.cmotion == U.cmotion.v or ctx.cmotion == U.cmotion.V then 38 | location = require('ts_context_commentstring.utils').get_visual_start_location() 39 | end 40 | 41 | return require('ts_context_commentstring').calculate_commentstring { 42 | key = type, 43 | location = location, 44 | } 45 | end 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /lua/ts_context_commentstring/integrations/vim_commentary.lua: -------------------------------------------------------------------------------- 1 | local gmap = vim.api.nvim_set_keymap 2 | local bmap = vim.api.nvim_buf_set_keymap 3 | 4 | for _, mode in ipairs { 'n', 'x', 'o' } do 5 | gmap( 6 | mode, 7 | 'ContextCommentary', 8 | [[v:lua.context_commentstring.update_commentstring_and_run('Commentary')]], 9 | { expr = true } 10 | ) 11 | end 12 | gmap( 13 | 'n', 14 | 'ContextCommentaryLine', 15 | [[v:lua.context_commentstring.update_commentstring_and_run('CommentaryLine')]], 16 | { expr = true } 17 | ) 18 | gmap( 19 | 'n', 20 | 'ContextChangeCommentary', 21 | [[v:lua.context_commentstring.update_commentstring_and_run('ChangeCommentary')]], 22 | { expr = true } 23 | ) 24 | 25 | local M = {} 26 | 27 | ---Set up vim-commentary mappings to first update the commentstring, and then 28 | ---run vim-commentary 29 | ---@param maps ts_context_commentstring.CommentaryConfig 30 | function M.set_up_maps(maps) 31 | if maps.Commentary then 32 | for _, mode in ipairs { 'n', 'x', 'o' } do 33 | bmap(0, mode, maps.Commentary, 'ContextCommentary', {}) 34 | end 35 | end 36 | if maps.CommentaryLine then 37 | bmap(0, 'n', maps.CommentaryLine, 'ContextCommentaryLine', {}) 38 | end 39 | if maps.ChangeCommentary then 40 | bmap(0, 'n', maps.ChangeCommentary, 'ContextChangeCommentary', {}) 41 | end 42 | if maps.CommentaryUndo then 43 | bmap(0, 'n', maps.CommentaryUndo, 'ContextCommentaryCommentary', {}) 44 | end 45 | end 46 | 47 | return M 48 | -------------------------------------------------------------------------------- /lua/ts_context_commentstring/internal.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | 3 | local utils = require 'ts_context_commentstring.utils' 4 | local config = require 'ts_context_commentstring.config' 5 | 6 | local M = {} 7 | 8 | ---Initialize the plugin in the buffer 9 | ---@param bufnr number 10 | function M.setup_buffer(bufnr) 11 | if not utils.is_treesitter_active(bufnr) then 12 | return 13 | end 14 | 15 | -- Save the original commentstring so that it can be restored later if there 16 | -- is no match 17 | api.nvim_buf_set_var(bufnr, 'ts_original_commentstring', api.nvim_buf_get_option(bufnr, 'commentstring')) 18 | 19 | local enable_autocmd = config.is_autocmd_enabled() 20 | 21 | -- If vim-commentary is installed, set up mappings for it 22 | if vim.g.loaded_commentary == 1 then 23 | require('ts_context_commentstring.integrations.vim_commentary').set_up_maps(config.config.commentary_integration) 24 | end 25 | 26 | if enable_autocmd then 27 | local group = api.nvim_create_augroup('context_commentstring_ft', { clear = true }) 28 | api.nvim_create_autocmd('CursorHold', { 29 | buffer = bufnr, 30 | group = group, 31 | desc = 'Change the commentstring on cursor hold using Treesitter', 32 | callback = function() 33 | require('ts_context_commentstring').update_commentstring() 34 | end, 35 | }) 36 | end 37 | end 38 | 39 | ---@class ts_context_commentstring.Args 40 | ---@field key string Key to prefer to be returned from ts_context_commentstring.CommentConfigMultiple 41 | ---@field location ts_context_commentstring.Location 42 | 43 | ---Calculate the commentstring based on the current location of the cursor. 44 | --- 45 | ---**Note:** We should treat this function like a public API, try not to break 46 | ---it! 47 | --- 48 | ---@param args? ts_context_commentstring.Args 49 | --- 50 | ---@return string | nil commentstring If found, otherwise `nil` 51 | function M.calculate_commentstring(args) 52 | args = args or {} 53 | local key = args.key or '__default' 54 | local location = args.location or nil 55 | 56 | local node, language_tree = utils.get_node_at_cursor_start_of_line( 57 | vim.tbl_keys(config.get_languages_config()), 58 | config.config.not_nested_languages, 59 | location 60 | ) 61 | 62 | if not node or not language_tree then 63 | return nil 64 | end 65 | 66 | local custom_calculation = config.config.custom_calculation 67 | if custom_calculation then 68 | local commentstring = custom_calculation(node, language_tree) 69 | if commentstring then 70 | return commentstring 71 | end 72 | end 73 | 74 | local language = language_tree:lang() 75 | local language_config = config.get_languages_config()[language] 76 | 77 | return M.check_node(node, language_config, key) 78 | end 79 | 80 | ---Update the `commentstring` setting based on the current location of the 81 | ---cursor. If no `commentstring` can be calculated, will revert to the ofiginal 82 | ---`commentstring` for the current file. 83 | --- 84 | ---**Note:** We should treat this function like a public API, try not to break 85 | ---it! 86 | --- 87 | ---@param args? ts_context_commentstring.Args 88 | function M.update_commentstring(args) 89 | local found_commentstring = M.calculate_commentstring(args) 90 | 91 | if found_commentstring then 92 | api.nvim_buf_set_option(0, 'commentstring', found_commentstring) 93 | else 94 | -- No commentstring was found, default to the default for this buffer 95 | local original_commentstring = vim.b.ts_original_commentstring 96 | if original_commentstring then 97 | api.nvim_buf_set_option(0, 'commentstring', vim.b.ts_original_commentstring) 98 | end 99 | end 100 | end 101 | 102 | ---Check if the given node matches any of the given types. If not, recursively 103 | ---check its parent node. 104 | --- 105 | ---@param node table 106 | ---@param language_config ts_context_commentstring.LanguageConfig 107 | ---@param commentstring_key string 108 | --- 109 | ---@return string | nil 110 | function M.check_node(node, language_config, commentstring_key) 111 | commentstring_key = commentstring_key or '__default' 112 | 113 | -- There is no commentstring configuration for this language, use the 114 | -- `ts_original_commentstring` 115 | if not language_config then 116 | return nil 117 | end 118 | 119 | -- The configuration is just a simple `commentstring` string, no need to do 120 | -- any extra Node traversal 121 | if type(language_config) == 'string' then 122 | return language_config 123 | end 124 | 125 | -- There is no node, we have reached the top-most node, use the default 126 | -- commentstring from language config 127 | if not node then 128 | return language_config[commentstring_key] or language_config.__default or language_config 129 | end 130 | 131 | local node_type = node:type() 132 | local match = language_config[node_type] 133 | 134 | if match then 135 | return match[commentstring_key] or match.__default or match 136 | end 137 | 138 | -- Recursively check the parent node 139 | return M.check_node(node:parent(), language_config, commentstring_key) 140 | end 141 | 142 | ---@deprecated 143 | function M.attach() 144 | vim.deprecate( 145 | 'context_commentstring nvim-treesitter module', 146 | "require('ts_context_commentstring').setup {} and set vim.g.skip_ts_context_commentstring_module = true to speed up loading", 147 | 'in the future (see https://github.com/JoosepAlviste/nvim-ts-context-commentstring/issues/82 for more info)', 148 | 'ts_context_commentstring' 149 | ) 150 | config.update(require('nvim-treesitter.configs').get_module 'context_commentstring') 151 | end 152 | 153 | ---@deprecated 154 | function M.detach() end 155 | 156 | _G.context_commentstring = {} 157 | 158 | ---Trigger re-calculation of the `commentstring` and trigger the given 159 | ---mapping right after that. 160 | --- 161 | ---This is in the global scope because 162 | ---`v:lua.require('ts_context_commentstring')` does not work for some reason. 163 | --- 164 | ---@param mapping string The Plug mapping to execute 165 | --- 166 | ---@return string 167 | function _G.context_commentstring.update_commentstring_and_run(mapping) 168 | M.update_commentstring() 169 | return api.nvim_replace_termcodes('' .. mapping, true, true, true) 170 | end 171 | 172 | return M 173 | -------------------------------------------------------------------------------- /lua/ts_context_commentstring/utils.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local fn = vim.fn 3 | 4 | ---Coordinates for a location. Both the line and the column are 0-indexed (e.g., 5 | ---line nr 10 is line 9, the first column is 0). 6 | ---@alias ts_context_commentstring.Location number[] 2-tuple of (line, column) 7 | 8 | local M = {} 9 | 10 | ---Get the location of the cursor to be used to get the treesitter node 11 | ---function. 12 | --- 13 | ---@return ts_context_commentstring.Location 14 | function M.get_cursor_location() 15 | local cursor = vim.api.nvim_win_get_cursor(0) 16 | return { cursor[1] - 1, cursor[2] } 17 | end 18 | 19 | ---Get the location of the cursor line first non-blank character. 20 | --- 21 | ---@return ts_context_commentstring.Location 22 | function M.get_cursor_line_non_whitespace_col_location() 23 | local cursor = api.nvim_win_get_cursor(0) 24 | local first_non_whitespace_col = fn.match(fn.getline '.', '\\S') 25 | 26 | return { 27 | cursor[1] - 1, 28 | first_non_whitespace_col, 29 | } 30 | end 31 | 32 | ---Get the location of the visual selection start. 33 | --- 34 | ---@return ts_context_commentstring.Location 35 | function M.get_visual_start_location() 36 | local first_non_whitespace_col = fn.match(fn.getline '.', '\\S') 37 | 38 | return { 39 | vim.fn.getpos("'<")[2] - 1, 40 | first_non_whitespace_col, 41 | } 42 | end 43 | 44 | ---Get the location of the visual selection end. 45 | --- 46 | ---@return ts_context_commentstring.Location 47 | function M.get_visual_end_location() 48 | return { 49 | vim.fn.getpos("'>")[2] - 1, 50 | vim.fn.getpos("'>")[3] - 1, 51 | } 52 | end 53 | 54 | ---@return boolean 55 | ---@param bufnr? number 56 | function M.is_treesitter_active(bufnr) 57 | bufnr = bufnr or 0 58 | 59 | -- get_parser will throw an error if Treesitter is not set up for the buffer 60 | local ok, _ = pcall(vim.treesitter.get_parser, bufnr) 61 | 62 | return ok 63 | end 64 | 65 | ---Get the node that is on the given location (default first non-whitespace 66 | ---character of the cursor line). This also handles injected languages via 67 | ---language tree. 68 | --- 69 | ---For example, if the cursor is at "|": 70 | --- |
71 | --- 72 | ---then will return the
node, even though it isn't at the cursor position 73 | --- 74 | ---Returns the node at the cursor's line and the language tree for that 75 | ---injection. 76 | --- 77 | ---@param only_languages string[] List of languages to filter for, all 78 | --- other languages will be ignored. 79 | ---@param not_nested_languages table List of languages which 80 | --- stop nesting 81 | ---@param location? ts_context_commentstring.Location location Line, column 82 | --- where to start traversing the tree. Defaults to cursor start of line. 83 | --- This usually makes the most sense when commenting the whole line. 84 | --- 85 | ---@return TSNode|nil node, vim.treesitter.LanguageTree|nil language_tree Node 86 | --- and language tree for the location 87 | function M.get_node_at_cursor_start_of_line(only_languages, not_nested_languages, location) 88 | if not M.is_treesitter_active() then 89 | return 90 | end 91 | 92 | location = location or M.get_cursor_line_non_whitespace_col_location() 93 | local range = { 94 | location[1], 95 | location[2], 96 | location[1], 97 | location[2], 98 | } 99 | 100 | -- default to top level language tree 101 | local language_tree = vim.treesitter.get_parser() 102 | -- Get the smallest supported language's tree with nodes inside the given range 103 | language_tree:for_each_tree(function(_, ltree) 104 | if 105 | ltree:contains(range) 106 | and vim.tbl_contains(only_languages, ltree:lang()) 107 | and not not_nested_languages[language_tree:lang()] 108 | then 109 | language_tree = ltree 110 | end 111 | end) 112 | 113 | local node = language_tree:named_node_for_range(range) 114 | return node, language_tree 115 | end 116 | 117 | return M 118 | -------------------------------------------------------------------------------- /plugin/ts_context_commentstring.lua: -------------------------------------------------------------------------------- 1 | if vim.g.loaded_ts_context_commentstring and vim.g.loaded_ts_context_commentstring ~= 0 then 2 | return 3 | end 4 | 5 | vim.g.loaded_ts_context_commentstring = 1 6 | 7 | local group = vim.api.nvim_create_augroup('ts_context_commentstring', { clear = true }) 8 | vim.api.nvim_create_autocmd('FileType', { 9 | group = group, 10 | desc = 'Set up nvim-ts-context-commentstring for each buffer that has Treesitter active', 11 | callback = function(args) 12 | require('ts_context_commentstring.internal').setup_buffer(args.buf) 13 | end, 14 | }) 15 | 16 | if not vim.g.skip_ts_context_commentstring_module or vim.g.skip_ts_context_commentstring_module == 0 then 17 | local nvim_ts_ok, nvim_ts = pcall(require, 'nvim-treesitter') 18 | if nvim_ts_ok then 19 | if not nvim_ts.define_modules then 20 | -- Running nvim-treesitter >= 1.0, modules are no longer a thing 21 | return 22 | end 23 | 24 | nvim_ts.define_modules { 25 | context_commentstring = { 26 | module_path = 'ts_context_commentstring.internal', 27 | }, 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | quote_style = "AutoPreferSingle" 4 | no_call_parentheses = true 5 | -------------------------------------------------------------------------------- /utils/minimal_init.lua: -------------------------------------------------------------------------------- 1 | -- Install lazy.nvim automatically 2 | local lazypath = vim.fn.stdpath 'data' .. '/lazy/lazy.nvim' 3 | if not vim.loop.fs_stat(lazypath) then 4 | vim.fn.system { 5 | 'git', 6 | 'clone', 7 | '--filter=blob:none', 8 | 'https://github.com/folke/lazy.nvim.git', 9 | '--branch=stable', -- latest stable release 10 | lazypath, 11 | } 12 | end 13 | vim.opt.rtp:prepend(lazypath) 14 | 15 | -- Or some other small value (Vim default is 4000) 16 | vim.opt.updatetime = 100 17 | 18 | require('lazy').setup { 19 | 'JoosepAlviste/nvim-ts-context-commentstring', 20 | { 21 | 'nvim-treesitter/nvim-treesitter', 22 | build = ':TSUpdate', 23 | config = function() 24 | require('nvim-treesitter.configs').setup { 25 | ensure_installed = { 'vim', 'lua' }, 26 | highlight = { 27 | enable = true, 28 | }, 29 | } 30 | end, 31 | }, 32 | { 33 | 'numToStr/Comment.nvim', 34 | config = function() 35 | require('Comment').setup { 36 | pre_hook = function() 37 | return vim.bo.commentstring 38 | end, 39 | } 40 | end, 41 | }, 42 | } 43 | 44 | -- Try commenting the following vimscript in and out with `gcc`, it should be 45 | -- commented with a double quote character 46 | vim.cmd [[ 47 | echo 'Hello World!' 48 | ]] 49 | -------------------------------------------------------------------------------- /utils/run_minimal.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | export XDG_CONFIG_HOME="${SCRIPT_DIR}/.testenv/config" 6 | export XDG_DATA_HOME="${SCRIPT_DIR}/.testenv/data" 7 | export XDG_STATE_HOME="${SCRIPT_DIR}/.testenv/state" 8 | export XDG_RUNTIME_DIR="${SCRIPT_DIR}/.testenv/run" 9 | export XDG_CACHE_HOME="${SCRIPT_DIR}/.testenv/cache" 10 | 11 | mkdir -p "${XDG_CONFIG_HOME}/nvim" 12 | mkdir -p "${XDG_DATA_HOME}/nvim" 13 | mkdir -p "${XDG_STATE_HOME}/nvim" 14 | mkdir -p "${XDG_RUNTIME_DIR}/nvim" 15 | mkdir -p "${XDG_CACHE_HOME}/nvim" 16 | 17 | nvim -u "${SCRIPT_DIR}/minimal_init.lua" "${SCRIPT_DIR}/minimal_init.lua" 18 | --------------------------------------------------------------------------------