Comment.nvim
Comment.nvim copied to clipboard
[Feature] Toggle mixed comments
Given a similar situation ↓ how to comment uncommented lines and uncomment commented ones?
printf("Line 1\n");
printf("Line 2\n");
// printf("Line 3\n");
// printf("Line 4\n");
Expected result: ↓
// printf("Line 1\n");
// printf("Line 2\n");
printf("Line 3\n");
printf("Line 4\n");
The result I get when I select these 4 lines and press gc: ↓
// printf("Line 1\n");
// printf("Line 2\n");
// // printf("Line 3\n");
// // printf("Line 4\n");
I think gc should toggle comments. To comment everything g> is already available...
This is expected. gc has to decide whether to comment or not, if it finds any line uncommented then it comments the whole motion/visual region. And to uncomment the whole region every line should be commented. Also, If you can check, this is the same behavior as tcomment, vim-commentary, and even VSCode.
What you are asking seems tempting at first but it is unexpected in practical :)
btw this functionality can be added for visual mode very easily. using :'<,'>normal gcc<CR> (as a mapping use :normal gcc<CR>). This will run gcc on each line separately, toggling each lines comments. To use it as a operator you will need to do a bit more work dealing with operatorfunc but you can base its functionality on the visual mode mapping
@IndianBoy42 That's perfect. Just a pointer, I anyone wish to implement custom keybindings I'll recommend using require('Comment').toggle() for this.
:'<,'>normal works on normal mode commands though, so you would have to do something like this:
map("x", "gt", ":normal :lua require'Comment'.toggle()<C-v><CR><CR>", nore)
The <C-v><CR> looks a bit jank to me, but its necessary
Or you can use :g like:
map("x", "gt", ":g/./lua require'Comment'.toggle()<CR><cmd>nohls<CR>", nore)
The g command sets highlights like / and :s so I'd recommend the nohls after othewise the entire file will be highlighted (because the pattern is .)
@xeluxee This is the best I can do to cover this case.
local A = vim.api
local U = require('Comment.utils')
function _G.__flip_flop_comment(vmode)
local lcs, rcs = U.unwrap_cstr(vim.bo.commentstring)
local scol, ecol, lines = U.get_lines(vmode, U.ctype.line)
local ll, rr = U.escape(lcs), U.escape(rcs)
local padding = true
for i, line in ipairs(lines) do
local is_commented = U.is_commented(line, ll, rr, padding)
if is_commented then
lines[i] = U.uncomment_str(line, ll, rr, padding)
else
lines[i] = U.comment_str(line, lcs, rcs, padding)
end
end
A.nvim_buf_set_lines(0, scol, ecol, false, lines)
end
A.nvim_set_keymap('x', '<leader>l', '<ESC><CMD>lua __flip_flop_comment(vim.fn.visualmode())<CR>', { noremap = true, silent = true })
Edit: This only works with linewise comment
Interesting, thanks
But how to use it in normal mode?
Something like gt3j to toggle comments on the next 3 lines?
@xeluxee You might wanna use operatorfunc :h operatorfunc.
This might work though I haven't tested
A.nvim_set_keymap('n', 'gt', '<CMD>set operatorfunc=v:lua.__flip_flop_comment<CR>g@', { noremap = true, silent = true })
This is the best I can do to cover this case.
This does not seems to work here...
First I'm receiving a warning Only has 3 variables, but you set 4 values on this line:
local is_commented = U.is_commented(line, ll, rr, padding)
and then there is an error if I'm using <leader>l while there is visual selection: E5108: Error executing lua ...ite/pack/packer/start/Comment.nvim/lua/Comment/utils.lua:259: attempt to concatenate local 'pp' (a boolean value)
So that now the API is stable. I am considering adding this but I am not sure whether to add a default mapping or just a give a lua api to play with.
Why not including it in extra mappings? A user can still disable it by setting <nop> if he don't like this feature
What will be the keymap's LHS? I'll be similar to gc as it will also wait for some motion like k, l. I am also assuming that the user will also want this "flip-flop" to be used inside visual mode.
I thought to gC but maybe someone else could give us a better suggestion
I am also assuming that the user will also want this "flip-flop" to be used inside visual mode.
Yeah it should be available in operator mode (gC4j) and in visual mode (V4jgC). I think it doesn't make sense to consider normal mode since to toggle comment on a single line gcc is already available.
What will be the keymap's LHS? I'll be similar to
gcas it will also wait for some motion likek,l. I am also assuming that the user will also want this "flip-flop" to be used inside visual mode.
Yeah this. need it in operator pending mode. Visual mode I don't care much but still good to have 👍🏽
Does it support this form ?
printf("Line 1\n"); printf("Line 2\n"); printf("Line 3\n"); printf("Line 4\n");
=>
/*
* printf("Line 1\n");
* printf("Line 2\n");
* printf("Line 3\n");
* printf("Line 4\n");
*/
I updated the custom keymap described in https://github.com/numToStr/Comment.nvim/issues/17#issuecomment-939413608 since the internal util functions seem to have changed a little bit.
-- NOTE: custom workaround until natively supported
local U = require('Comment.utils')
function _G.__flip_flop_comment(opmode)
local vmark_start = vim.api.nvim_buf_get_mark(0, '<')
local vmark_end = vim.api.nvim_buf_get_mark(0, '>')
local range = U.get_region(opmode)
local lines = U.get_lines(range)
local ctx = {
ctype = U.ctype.line,
range = range,
}
local cstr = require('Comment.ft').calculate(ctx) or vim.bo.commentstring
local lcs, rcs = U.unwrap_cstr(cstr)
local ll, rr = U.escape(lcs), U.escape(rcs)
local padding, pp = U.get_padding(true)
local min_indent = nil
for _, line in ipairs(lines) do
if not U.is_empty(line) and not U.is_commented(ll, rr, pp)(line) then
local cur_indent = U.grab_indent(line)
if not min_indent or #min_indent > #cur_indent then
min_indent = cur_indent
end
end
end
for i, line in ipairs(lines) do
local is_commented = U.is_commented(ll, rr, pp)(line)
if line == "" then
elseif is_commented then
lines[i] = U.uncomment_str(line, ll, rr, pp)
else
lines[i] = U.comment_str(line, lcs, rcs, padding, min_indent)
end
end
vim.api.nvim_buf_set_lines(0, range.srow - 1, range.erow, false, lines)
vim.api.nvim_buf_set_mark(0, '<', vmark_start[1], vmark_start[2], {})
vim.api.nvim_buf_set_mark(0, '>', vmark_end[1], vmark_end[2], {})
end
vim.keymap.set({ 'n', 'x' }, 'gC', '<cmd>set operatorfunc=v:lua.__flip_flop_comment<cr>g@', { silent = true, desc = "toggle the comment state of each line individually" })
It is not perfect but it does the job for me. Although it would be nicer if this was provided as a extra keymap.
It is not perfect but it does the job for me
Nice, thank you! I've only found two issues using your function:
- Indentation: while commenting a block with different indentations comments are put before the first consistent character of every line. This leads formatters to align all these lines, breaking the original indentation. This doesn't happen with regular
gc - Visual selection: after selecting some text and pressing
gCeverything works fine, but selection marks are edited. So if I pressgvonly a smaller part of the previous selection gets selected. This doesn't happen with regulargc
@xeluxee I quickly updated (by borrowing the indentation code from the actual plugin) it to address the issues you mentioned, hopefully that makes it also good enough for you as a bridging solution until its in the plugin itself (hopefully).
@xeluxee I quickly updated (by borrowing the indentation code from the actual plugin) it to address the issues you mentioned, hopefully that makes it also good enough for you as a bridging solution until its in the plugin itself (hopefully).
Now it works fine, thanks 😄
Hello,
I was a user of NERDCommenter which used the terminology invert for this which I quite like, the mapping was ending with i, to show it's different than the toggle action.
For us, this could map to gci ?
For us, this could map to
gci?
No, because it would conflict with gci{motion} like gciw, gcip etc.
Oh right ><
gi ? (but that's also a useful builtin..)
What about no builtin mapping and let people set one if needed?
Is there a blocker for this to end up in the plugin ? (except finding a mapping for it :eyes:)
What about no builtin mapping and let people set one if needed?
That's also my thought to just expose the API.
Is there a blocker for this to end up in the plugin ?
$DAYJOB and time :)
Temporary solution with vmap:
vmap <silent> gC :normal gcc<cr>
@daangoossens22 Thanks for sharing your solution! Unfortunately, I get an error when I try it. Any chance you have an updated version you could share, if you also hit this?
E5108: Error executing lua ...ite/pack/packer/start/Comment.nvim/lua/Comment/utils.lua:149: bad argument #1 to 'match' (string expected, got table)
stack traceback:
[C]: in function 'match'
...ite/pack/packer/start/Comment.nvim/lua/Comment/utils.lua:149: in function 'unwrap_cstr'
@numToStr is there a way to determine if a line is commented using new APIs? Unfortunately @daangoossens22's solution uses internal functions that are likely to be changed, leading to unexpected errors
@xeluxee You can use :h comment.utils.is_commented
require'Comment.utils'.is_commented('--', '', true)(' -- Hello')
(I hope to start working on this after neovim 0.8 release which is just couple of days away)
Updated function to invert (flip flop) comments
---Operator function to invert comments on each line
function _G.__flip_flop_comment()
local U = require("Comment.utils")
local s = vim.api.nvim_buf_get_mark(0, "[")
local e = vim.api.nvim_buf_get_mark(0, "]")
local range = { srow = s[1], scol = s[2], erow = e[1], ecol = e[2] }
local ctx = {
ctype = U.ctype.linewise,
range = range,
}
local cstr = require("Comment.ft").calculate(ctx) or vim.bo.commentstring
local ll, rr = U.unwrap_cstr(cstr)
local padding = true
local is_commented = U.is_commented(ll, rr, padding)
local rcom = {} -- ranges of commented lines
local cl = s[1] -- current line
local rs, re = nil, nil -- range start and end
local lines = U.get_lines(range)
for _, line in ipairs(lines) do
if #line == 0 or not is_commented(line) then -- empty or uncommented line
if rs ~= nil then
table.insert(rcom, { rs, re })
rs, re = nil, nil
end
else
rs = rs or cl -- set range start if not set
re = cl -- update range end
end
cl = cl + 1
end
if rs ~= nil then
table.insert(rcom, { rs, re })
end
local cursor_position = vim.api.nvim_win_get_cursor(0)
local vmark_start = vim.api.nvim_buf_get_mark(0, "<")
local vmark_end = vim.api.nvim_buf_get_mark(0, ">")
---Toggle comments on a range of lines
---@param sl integer: starting line
---@param el integer: ending line
local toggle_lines = function(sl, el)
vim.api.nvim_win_set_cursor(0, { sl, 0 }) -- idk why it's needed to prevent one-line ranges from being substituted with line under cursor
vim.api.nvim_buf_set_mark(0, "[", sl, 0, {})
vim.api.nvim_buf_set_mark(0, "]", el, 0, {})
require("Comment.api").locked("toggle.linewise")("")
end
toggle_lines(s[1], e[1])
for _, r in ipairs(rcom) do
toggle_lines(r[1], r[2]) -- uncomment lines twice to remove previous comment
toggle_lines(r[1], r[2])
end
vim.api.nvim_win_set_cursor(0, cursor_position)
vim.api.nvim_buf_set_mark(0, "<", vmark_start[1], vmark_start[2], {})
vim.api.nvim_buf_set_mark(0, ">", vmark_end[1], vmark_end[2], {})
vim.o.operatorfunc = "v:lua.__flip_flop_comment" -- make it dot-repeatable
end
-- Invert (flip flop) comments with gC, in normal and visual mode
vim.keymap.set(
{ "n", "x" },
"gC",
"<cmd>set operatorfunc=v:lua.__flip_flop_comment<cr>g@",
{ silent = true, desc = "Invert comments" }
)
TY @xeluxee! :partying_face:
Thanks a lot @xeluxee. I want that function as well. :clap: