Comment.nvim icon indicating copy to clipboard operation
Comment.nvim copied to clipboard

[Feature] Toggle mixed comments

Open xeluxee opened this issue 4 years ago • 29 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...

xeluxee avatar Oct 09 '21 19:10 xeluxee

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 :)

numToStr avatar Oct 10 '21 04:10 numToStr

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 avatar Oct 10 '21 05:10 IndianBoy42

@IndianBoy42 That's perfect. Just a pointer, I anyone wish to implement custom keybindings I'll recommend using require('Comment').toggle() for this.

numToStr avatar Oct 10 '21 05:10 numToStr

:'<,'>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 .)

IndianBoy42 avatar Oct 10 '21 06:10 IndianBoy42

@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

numToStr avatar Oct 10 '21 06:10 numToStr

Interesting, thanks But how to use it in normal mode? Something like gt3j to toggle comments on the next 3 lines?

xeluxee avatar Oct 10 '21 15:10 xeluxee

@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 })

numToStr avatar Oct 10 '21 15:10 numToStr

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)

VKondakoff avatar Nov 03 '21 20:11 VKondakoff

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.

numToStr avatar Jan 09 '22 10:01 numToStr

Why not including it in extra mappings? A user can still disable it by setting <nop> if he don't like this feature

xeluxee avatar Jan 09 '22 10:01 xeluxee

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.

numToStr avatar Jan 09 '22 10:01 numToStr

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.

xeluxee avatar Jan 09 '22 11:01 xeluxee

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.

Yeah this. need it in operator pending mode. Visual mode I don't care much but still good to have 👍🏽

kuntau avatar Jan 09 '22 12:01 kuntau

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");
*/

hawkinchina avatar Apr 06 '22 11:04 hawkinchina

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.

daangoossens22 avatar Jun 07 '22 12:06 daangoossens22

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 gC everything works fine, but selection marks are edited. So if I press gv only a smaller part of the previous selection gets selected. This doesn't happen with regular gc

xeluxee avatar Jun 07 '22 14:06 xeluxee

@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).

daangoossens22 avatar Jun 07 '22 15:06 daangoossens22

@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 😄

xeluxee avatar Jun 08 '22 17:06 xeluxee

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 ?

bew avatar Aug 25 '22 05:08 bew

For us, this could map to gci?

No, because it would conflict with gci{motion} like gciw, gcip etc.

numToStr avatar Aug 25 '22 05:08 numToStr

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:)

bew avatar Aug 25 '22 05:08 bew

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 :)

numToStr avatar Aug 25 '22 05:08 numToStr

Temporary solution with vmap:

vmap <silent> gC :normal gcc<cr>

hiberabyss avatar Sep 02 '22 08:09 hiberabyss

@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'

mmirus avatar Sep 29 '22 17:09 mmirus

@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 avatar Sep 29 '22 19:09 xeluxee

@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)

numToStr avatar Sep 30 '22 05:09 numToStr

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" }
)

xeluxee avatar Oct 05 '22 16:10 xeluxee

TY @xeluxee! :partying_face:

mmirus avatar Oct 11 '22 14:10 mmirus

Thanks a lot @xeluxee. I want that function as well. :clap:

OliverChao avatar Dec 10 '22 06:12 OliverChao