jake-stewart / multicursor.nvim

multiple cursors in neovim
MIT License
122 stars 1 forks source link

cursor management api #7

Closed jake-stewart closed 3 days ago

jake-stewart commented 2 weeks ago

need an api so power users like @mikavilpas can write match/split/align functionality easily in their configs

will expand and refine this list later

mikavilpas commented 2 weeks ago

This sounds great 👍🏻

I suppose perform would be for performing a motion? Currently I tried out visualToCursors() but it looks like the cursors always start at column 0. What I'm trying to do is press I (capital i) to add them to the starts of visual lines, and A for the ends.

jake-stewart commented 2 weeks ago

This sounds great 👍🏻

I suppose perform would be for performing a motion? Currently I tried out visualToCursors() but it looks like the cursors always start at column 0. What I'm trying to do is press I (capital i) to add them to the starts of visual lines, and A for the ends.

perform would be a motion or function which applies to each cursor.

once i have the api out i imagine it would be as easy as mapping I to visualToCursors() and then perform('^').

jake-stewart commented 1 week ago

api coming along nicely.

you will be able to access it through mc.action, which provides a context. this lets users perform multiple complex actions and queries, only redrawing the cursors & updating the undolist once the function finishes.

cursors will have metatables with many useful methods for querying and manipulating cursor state.

here is how the column transpose logic is written with the new api:

local function transposeCursors(direction)
    mc.action(function(ctx)
        ctx:forEach(function(cursor)
            cursor:convertToSingleLines()
        end)
        local values = ctx:map(function(cursor)
            return cursor:getVisualLines()[1]
        end)
        ctx:forEach(function(cursor, i)
            local idx = ((i - direction - 1) % #values) + 1
            cursor:perform('"_c' .. values[idx] .. TERM_CODES.ESC .. "v`<")
        end)
        ctx:rotateSelectedCursor(direction)
    end)
end

it's a lot better.

will have this api out tomorrow

jake-stewart commented 1 week ago

i've pushed changes to the api branch. i'll test it over the next couple of days to make sure no bugs are introduced before moving to main.

@mikavilpas the changes you talked about in #6 can now be implemented like so:

vim.keymap.set("v", "I", function()
    mc.visualToCursors()
    mc.perform("^")
    mc.feedkeys("I")
end)

vim.keymap.set("v", "A", function()
    mc.visualToCursors()
    mc.perform("$")
    mc.feedkeys("A")
end)

vim.keymap.set("v", "<leader><esc>", function()
    local mode = vim.fn.mode()
    mc.visualToCursors()
    if (mode == "V" or mode == "v") then
        mc.perform("^")
    end
end)

meanwhile, more low level things like match, select, align, transpose can all be (and all are) written with the new cursor api. here are some examples

function mc.transposeCursors(direction)
    mc.action(function(ctx)
        ctx:forEach(function(cursor)
            cursor:convertToSingleLines()
        end)
        local values = ctx:map(function(cursor)
            return cursor:getVisualLines()[1]
        end)
        ctx:forEach(function(cursor, i)
            local idx = ((i - direction - 1) % #values) + 1
            cursor:perform('"_c' .. values[idx] .. TERM_CODES.ESC .. "v`<o")
        end)
        ctx:findNextCursor(ctx:getMainCursor():getPos(), direction):select()
    end)
end
function mc.matchCursors(pattern)
    mc.action(function(ctx)
        pattern = pattern or vim.fn.input("Match: ")
        if not pattern or pattern == "" then
            return
        end
        ctx:forEach(function(cursor)
            cursor:convertToSingleLines()
        end)
        ctx:forEach(function(cursor)
            local selection = cursor:getVisualLines()
            local matches = matchlist(selection, pattern, { userConfig = true })
            local visual = cursor:getVisual()
            local line, col = table.unpack(visual[1])
            for _, match in ipairs(matches) do
                if #match.text > 0 then
                    local newCursor = cursor:clone()
                    newCursor:setVisual({
                        line,
                        col + match.byteidx + #match.text - 1,
                        line,
                        col + match.byteidx
                    })
                    newCursor:setMode("n")
                end
            end
            cursor:delete()
        end)
    end)
end

still need to write up docs for this. currently i've added these functions but i will probably need more and may rename them.

--- @return number
function Cursor:line()

--- @return number
function Cursor:col()

--- @return string
function Cursor:getLine()

function Cursor:delete()

-- sets the main cursor to this one
function Cursor:select()

--- @return boolean
function Cursor:atVisualStart()

-- creates a new cursor for each visual line and deletes this one
function Cursor:convertToSingleLines()

--- @return boolean
function Cursor:isMainCursor()

--- @return [integer, integer]
function Cursor:getPos()

--- @param pos [integer, integer]
function Cursor:setPos(pos)

--- @return Cursor
function Cursor:clone()

--- @return string[]
function Cursor:getVisualLines()

--- @return string[]
function Cursor:getFullVisualLines()

--- @return string
function Cursor:mode()

--- @param mode string
function Cursor:setMode(mode)

--- @param action string | function
--- @param opts? { remap: boolean }
function Cursor:perform(action, opts)

--- @param visual [integer, integer, integer, integer]
function Cursor:setVisual(visual)

--- @return boolean
function Cursor:inVisualMode()

meanwhile the CursorContext has methods like forEach, map, getMainCursor, getFirstCursor, getLastCursor. again subject to change

mikavilpas commented 1 week ago

The new api is excellent, thanks a lot!

One question though, is it possible to make the multi cursors enter insert mode? So far I did not find a way to do this, but other than this everything was 99% less code :)

jake-stewart commented 1 week ago

The new api is excellent, thanks a lot!

One question though, is it possible to make the multi cursors enter insert mode? So far I did not find a way to do this, but other than this everything was 99% less code :)

insert mode is tricky since the way it is considered a single motion (it is dot repeatable). i record the keys and then play them for each cursor. therefore it doesn't make much sense to tell a cursor to enter insert mode and stay in it programatically.

instead, it's better to use the full cursor:perform("ihello" .. esc) or, you can use the mc.feedkeys("ihello") which will queue up ihello for the cursors, applying it to each once you leave insert mode. like shown below:

vim.keymap.set("v", "I", function()
    mc.visualToCursors()
    mc.perform("^")
    mc.feedkeys("I")
end)

could you explain what you are trying to achieve if neither of these work for you, thanks.

i am still working on the api. it now supports getCursors, instead of forcing you to use map/forEach. i will probably my changes tonight.

mikavilpas commented 1 week ago

Sure - what I'm trying to do is the same effect you get in visual block mode with capital I:

https://github.com/user-attachments/assets/7257d679-8c09-4cfe-8b00-8032f3ec7a87

Here I have it working but I have to add a i manually to go to insert mode now. It's not the end of the world though, so if there's no easy way of fixing it, I think it can also be left unfixed.

jake-stewart commented 1 week ago

@mikavilpas maybe i explained myself poorly. does this achieve what you're after?

local CTRL_V = vim.api.nvim_replace_termcodes("<c-v>", true, true, true);

vim.keymap.set("v", "I", function()
    local mode = vim.fn.mode()
    mc.visualToCursors()
    if mode == CTRL_V then
        mc.feedkeys("i")
    else
        mc.perform("^")
        mc.feedkeys("I")
    end
end)
jake-stewart commented 1 week ago

also for this task i prefer just doing

vim.keymap.set({"n", "v"}, "<down>", function() mc.addCursor("j") end)

and then just hit <down> a bunch of times. for me it's mapped to <c-j>

jake-stewart commented 3 days ago

it is now merged into main. readme has documentation.

also @mikavilpas i added mc.appendVisual and mc.insertVisual

vim.keymap.set("v", "I", mc.insertVisual)
vim.keymap.set("v", "A", mc.appendVisual)