# ts-comments.nvim: comment strings that follow the treesitter node, not the file

> ts-comments.nvim is a small Neovim plugin that gives each treesitter language, and sometimes each node type, its own comment string, with relaxed uncommenting rules and proper whitespace handling. The default configuration already covers more than thirty languages, and the per-node overrides are the interesting part.

**folke/ts-comments.nvim** — Tiny plugin to enhance Neovim's native comments

- Repository: https://github.com/folke/ts-comments.nvim
- Stars: 571 · Forks: 29
- Language: Lua
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/folke-ts-comments-nvim

## What the plugin actually changes

ts-comments.nvim is a tiny plugin to enhance Neovim's native comments, and the feature list is five items.

It lets you easily override the comment string for a given treesitter language. It supports different comment strings for different treesitter node types. It has relaxed rules for uncommenting. It adds proper whitespace when needed. And it supports multiple comment strings for the same language, where the first one is used for adding comments and all of them are used for uncommenting.

That last point is the design that makes the plugin worth having, and it is the part that is easy to miss. A language like JavaScript has more than one legitimate comment style, and the choice depends on the node. A block comment is right for a JSX element, because a line comment inside a JSX attribute list is not valid. A line comment is right for a call expression. The plugin lets you say that, and it lets you uncomment either form when you come back to it.

The requirement is Neovim 0.10.0 or newer, and the plugin is written in Lua, Apache-2.0 licensed, with the last push on 2026-06-29. The most recent tagged release is v1.5.0 from 2024-07-24, preceded by v1.4.0 in July 2024 and v1.3.0 earlier that month.

## Installation is a lazy.nvim spec with a version guard

The lazy.nvim spec in the README is short, and it contains a detail worth copying:

```lua
{
  "folke/ts-comments.nvim",
  opts = {},
  event = "VeryLazy",
  enabled = vim.fn.has("nvim-0.10.0") == 1,
}
```

The enabled check uses vim.fn.has with the nvim-0.10.0 feature string, so the plugin disables itself on an older Neovim rather than throwing an error at startup. Since the plugin requires 0.10.0, that guard is exactly the right shape: a version too old gets a plugin that is quietly off instead of a broken keymap.

The event of VeryLazy means the plugin loads on the VeryLazy event rather than at startup, which suits something this small.

The opts value is empty by default, and that is the point rather than an omission. The plugin uses the default Neovim commentstring as a fallback, so there is no need to configure every language. You configure the exceptions, and everything else keeps the behaviour you already had.

The repository also carries the usual plugin hygiene: a doc/ directory, a CHANGELOG.md, a .editorconfig, a .neoconf.json for neoconf, a selene.toml for the Lua linter, a stylua.toml for the formatter, a tests/ directory, a scripts/ directory and a vim.yml.

## The default config is a lookup table, not a set of rules

The default configuration is a single table keyed by language, and reading it tells you most of what the plugin does.

Most entries are a plain string with a %s placeholder, which is the comment string with the text position marked. The C family all use // %s, and so do Rust, PHP, ReScript, Gleam, KDL, F# and CUE. The shell and Lisp family uses # %s, which covers GraphQL, HCL, Terraform, Rego, IPython notebooks and Hyprlang. HTML, Vue, Svelte, Astro, Axaml, XAML and the CS project file use the HTML comment form. The Lua-adjacent entries use -- %s for SQL, ; %s for INI, ;; %s with a ; %s alternative for Clojure, and the Handlebars and Glimmer forms use the mustache comment syntax. Twig uses its own comment delimiters, Templ and PHP have their own entries, and the styled language uses the CSS block comment form.

Then there are the multi-value entries, and they are where the plugin earns its keep.

Rust has both the line and the block form. Clojure has the two semicolon forms. JavaScript, TypeScript and TSX each carry the line form as the default, the block form as a second alternative for uncommenting, and then a set of node specific overrides: call_expression, jsx_attribute, jsx_element, jsx_fragment, spread_element and statement_block. Templ has the line form as default and a component_block override that uses the HTML comment form.

## Node overrides are the part that changes how a file reads

The JavaScript and TypeScript entries are worth reading in full, because they are the clearest statement of the plugin's purpose:

```lua
    javascript = {
      "// %s", -- default commentstring when no treesitter node matches
      "/* %s */",
      call_expression = "// %s", -- specific commentstring for call_expression
      jsx_attribute = "// %s",
      jsx_element = "{/* %s */}",
      jsx_fragment = "{/* %s */}",
      spread_element = "// %s",
      statement_block = "// %s",
    },
```

The first entry in the list is the default when no treesitter node matches, and the second is an additional string that is also accepted when uncommenting. After that, each named key is a specific treesitter node type with its own string.

The two JSX entries are the interesting ones, and they are the reason a generic comment string is wrong for JavaScript. A jsx_element is a JSX tag, and a line comment cannot be placed inside the angle brackets of a tag, so the string used is a block comment wrapped in braces, {/* %s */}. A jsx_fragment, the long form of a fragment, gets the same treatment. But a jsx_attribute, which is a single attribute on a tag, gets a line comment, because the attribute sits in a position where a line comment is legal.

So one file, one language, and three different comment strings depending on where the cursor is. That is the thing the built-in commentstring cannot express, and it is the reason the plugin exists rather than being a mapping of languages to comment styles.

## Relaxed uncommenting and whitespace, and how they interact

Two of the five features are about what happens when you take a comment back off.

Relaxed rules for uncommenting is the one that saves time in practice. Native Neovim uncommenting is strict about the exact comment string it wrote, so if the text has been edited, or if the comment was written by a different tool, or if it uses one of several acceptable forms, the native behaviour often leaves the text alone. This plugin accepts any of the strings configured for that language, and with several strings per language, all of them are tried.

Add proper whitespace when needed is about the other direction. When you insert a comment, the string has to be placed so the result is valid, which sometimes means adding a space that was not there, and the plugin handles that rather than leaving you with a comment that runs into the code.

Neither of these is a headline feature, and both are the reason the plugin is described as enhancing native comments rather than replacing them. The native machinery still does the work; the plugin decides which strings are legal and in which contexts.

The related project link at the end of the README points to nvim-ts-context-commentstring by JoosepAlviste, which is the plugin that solves the same problem in the other direction, by using the treesitter context to pick a comment string for the language you already have. If you are choosing between the two, that is the comparison to make.

## Where this sits against a language specific comment plugin

The alternative to a general plugin like this one is a per-language plugin, or setting the commentstring per filetype in your own configuration.

The per-language approach is the traditional one. A filetype plugin sets commentstring in a filetype autocmd, and the language has one comment style, which is right for the majority of languages because most languages have exactly one comment syntax that matters. The weakness appears when a language has several, and when the choice depends on position in the file rather than on the file's type.

The filetype approach cannot express the JavaScript case. A JSX file is one filetype, and the comment string it needs at a jsx_element is different from the one it needs at a call_expression, and a single commentstring value cannot be both. The per-node approach can, because it looks at the syntax tree.

The other alternative is the context plugin the README links, which works from the language's existing commentstring and adds the context detection on top. The difference in approach between the two is where the source of truth sits. This plugin owns the whole table, so you can change a language's default comment style as well as its per-node behaviour. The context plugin keeps your language's commentstring and only adjusts the context, so it changes less and requires less configuration.

Which is better depends on how opinionated you want to be. If your project's comment style is already settled, the context plugin is lighter. If you want a single table that says exactly what a jsx_element should look like, this plugin is the clearer place to put it.

## Conclusion

Adopt ts-comments.nvim if you work in a language where the right comment string depends on the syntax tree rather than the file, most obviously JavaScript and TypeScript with JSX, and you want one configuration for it rather than per-buffer overrides. Do not adopt it for a project with a single comment style, because the plugin uses the default Neovim commentstring as a fallback and does nothing extra in that case. Before you configure it, check the per-node entries rather than the per-language ones, since a jsx_element needs a different string from a call_expression in the same file, and note that the last tagged release is v1.5.0 from 2024-07-24 while the last push was on 2026-06-29.

## FAQ

### What does ts-comments.nvim do?

It enhances Neovim's native comments by letting each treesitter language, and each treesitter node type, have its own comment string. It also has relaxed rules for uncommenting and adds proper whitespace when a comment is inserted.

### How do I install ts-comments.nvim with lazy.nvim?

Add the plugin spec with opts = {}, event = "VeryLazy" and enabled = vim.fn.has("nvim-0.10.0") == 1. The version guard disables the plugin on a Neovim older than the 0.10.0 the plugin requires, and the empty opts are fine because the plugin falls back to the default Neovim commentstring.

### Can ts-comments.nvim use different comment strings for the same language?

Yes. A language entry can be a list, where the first string is used when adding comments and all of them are used when uncommenting. JavaScript, TypeScript and TSX also carry per-node entries such as call_expression, jsx_attribute, jsx_element, jsx_fragment, spread_element and statement_block.

### What Neovim version does ts-comments.nvim need?

Neovim 0.10.0 or newer. The lazy.nvim spec in the README guards the plugin with a vim.fn.has check for nvim-0.10.0 so it stays disabled on older versions.

## Sources

- [folke/ts-comments.nvim on GitHub](https://github.com/folke/ts-comments.nvim)
- [Issues](https://github.com/folke/ts-comments.nvim/issues)
- [License: Apache-2.0](https://github.com/folke/ts-comments.nvim/blob/main/LICENSE)
- [README](https://github.com/folke/ts-comments.nvim/blob/main/README.md)
- [Releases](https://github.com/folke/ts-comments.nvim/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/folke-ts-comments-nvim
