jake-stewart / multicursor.nvim

multiple cursors in neovim
MIT License
629 stars 6 forks source link

How to swap text regions #68

Closed swarn closed 22 hours ago

swarn commented 2 days ago

Hi Jake, nice plugin!

This isn't so much an "issue" as a question/feedback; please ignore/delete as desired. Maybe a wiki or discussions for the repo?

I'd like to implement swapCursors. Similar notion to the transpose function, but simply exchanges the selected region under the main cursor with an adjacent region. Also, I'd like it to work with multiple line selections. So given the following text

p1 text text
text

p2 text text

p3 text text
text
text

it would be easy to split into paragraphs, then reorder the paragraphs.

The transpose example is good, but only works with single lines. It seems like calling nvim_buf_set_text in an mc.action doesn't get repeated. So I thought I'd use a few registers. What I have is

local function swapCursors(direction)
  mc.action(function(ctx)
    local thisCursor = ctx:mainCursor()
    local thisPos = thisCursor:getPos()

    local otherCursor
    if direction == -1 then
      otherCursor = ctx:prevCursor(thisPos)
    else
      otherCursor = ctx:nextCursor(thisPos)
    end
    if not otherCursor then return end

    thisCursor:feedkeys([["zygv]])
    otherCursor:feedkeys([["yygv]])

    thisCursor:feedkeys('"yp`]v`[')
    otherCursor:feedkeys('"zp`]v`[')

    otherCursor:select()
  end)
end

This works the first time I call it, but has errors on multi-line selections after the first time. I think this is something to do with restoring the visual selection at the cursor after the paste operation.

How about a cursor:setLines() method :).

jake-stewart commented 2 days ago

Hi swarn

there are no guidelines for issues/contributing because i find them annoying.

i've added Cursor:setVisualLines(string[]) which does the job:

local thisLines = thisCursor:getVisualLines()
local otherLines = otherCursor:getVisualLines()

thisCursor:setVisualLines(otherLines)
otherCursor:setVisualLines(thisLines)

works very well for visual char and line mode. visual block mode needs some work but looks very complicated.

try :Lazy update and let me know if any issues.

swarn commented 2 days ago

Nice!

The API works well. This swap function is now very clean. Another example: I was happy to write a "run arbitrary Ex command at cursors" function and find that it is as simple as:

local function runCmdAtCursors(command)
  mc.action(function(ctx)
    command = command or vim.fn.input "Command: "
    if not command or command == "" then return end

    ctx:forEachCursor(function(cursor)
      cursor:perform(function() vim.cmd(command) end)
    end)
  end)
end
swarn commented 2 days ago

I should have tested more before commenting!

This works for selections on different lines, but not selections on the same line — which I know, I didn't mention earlier. Starting here:

Sometimes the result is this:

And somtimes it just throws an error:

...multicursor.nvim/lua/multicursor-nvim/cursor-manager.lua:1208: attempt to index field '_redoChangePos' (a nil value)

The code for the above is what you expect, but for the sake of completeness:

swapCursors code ```lua local function swapCursors(direction) mc.action(function(ctx) local thisCursor = ctx:mainCursor() local thisPos = thisCursor:getPos() local otherCursor if direction == -1 then otherCursor = ctx:prevCursor(thisPos) else otherCursor = ctx:nextCursor(thisPos) end if not otherCursor then return end local thisLines = thisCursor:getVisualLines() local otherLines = otherCursor:getVisualLines() thisCursor:setVisualLines(otherLines) otherCursor:setVisualLines(thisLines) otherCursor:select() end) end ```

I've come up with a contrived example that I think covers everything. Starting with the following text:

You can select all, then match ( to create cursors, then vi) (from mini-ai) to select inside parentheses including whitespace. By the way, I like how this plugin instantly works with custom textobjects.

The swap function using the new setVisualLines method results in:

The selection of the 222 region has vanished.

Instead we'd like to see:

After two swaps back:

And this would be the result after three swaps:

The reason I'm hoping for this super-generalized behavior is that multicursor can generalize and replace a lot of "splitjoin" and swap functions.

jake-stewart commented 1 day ago

@swarn I have pushed out a new fix. try :Lazy update

Seems to work well for cursors on the same line. I also made undo/redo work properly. Let me know if any issues.

Glad you like the api. Since I wrote all the examples/default behaviours with the same api it is quite capable.

swarn commented 1 day ago

@jake-stewart, this looks good. It works for swapping the words in a line:

aaa bbb ccc

As well as paragraphs:

aaa aaa
aaaa

bbb

ccc
ccc
ccc

It still struggles with the contrived, mixed example above. Starting with

Selecting inside parens

One swap backwards isn't quite right: it lost the trailing newline for the 4 block. Also, the 4 block selection now includes the leading paren.

Which we can see if we swap backwards again

Also, undo doesn't work properly after that second swap. It looks like this:

jake-stewart commented 1 day ago

@swarn should be good for you now

swarn commented 1 day ago

💯

It works for all the tests I can come up with.

I see you're feeding the lines into a register and pasting it, then restoring the register. Is the feedkeys approach better than trying to use nvim_buf_set_text? I tried to use the latter without success.

jake-stewart commented 22 hours ago

by using feedkeys, i let vim handle how replacing a visual selection works. i pass l, b or c to vim.fn.setreg to define that the lines are line/block/char. contrast to nvim_buf_set_text where i would need to call it per each line of a visual block selection.

replacing a visual selection with P automatically sets the change marks which i use to set the new cursor position and visual selection.

it would be very tricky with nvim_buf_set_text since i would need to manually calculate the new visual selection. for visual char mode, the visual end column is the length of the last line, unless the visual selection spans one line then it is the visual start column + length of the last line. similar edge cases exist for the other visual modes.

also nvim_buf_set_text does not allow including the eol character in a range since it is considered "out of range". i would need to handle this case by decreasing the end line by one and setting end column to zero.

swarn commented 22 hours ago

Ah, it makes to use P and let vim handle the bookkeeping for you. Thanks for explaining!

jake-stewart commented 22 hours ago

@swarn i have added swapCursors to the examples.

sidenote, i added seekCursor and seekBoundaryCursor, which simplifies the code a little bit more.

--- @param direction -1 | 1
--- @param wrap? boolean
function examples.swapCursors(direction, wrap)
    mc.action(function(ctx)
        local mainCursor = ctx:mainCursor()
        local otherCursor = ctx:seekCursor(
            mainCursor:getPos(), direction, wrap)
        if otherCursor and otherCursor ~= mainCursor then
            local mainLines = mainCursor:getVisualLines()
            local otherLines = otherCursor:getVisualLines()
            mainCursor:setVisualLines(otherLines)
            otherCursor:setVisualLines(mainLines)
            otherCursor:select()
        end
    end)
end