stevearc / stickybuf.nvim

Neovim plugin for locking a buffer to a window
MIT License
246 stars 7 forks source link

Save and restore buffer name fix #19

Closed eyalz800 closed 1 year ago

eyalz800 commented 1 year ago

I had an issue with symbols-outline where it expected to be able to immediately create new buffer with the same name - and so I ended up renaming the buffer in the bufhidden override

stevearc commented 1 year ago

I'm hesitant to make this change because it's going to affect all buffers, not just symbols-outline, and it's difficult to tell what all the side effects would be.

Does symbols-outline need the buffer to have a particular name? Could you solve this instead by just renaming the buffer with an autocmd to something random when it's created?

eyalz800 commented 1 year ago

I went for this because this was the simplest solution, I have not explored autocmd but it sounded like it could leave an opening for an error there.

Since the plugin pretends to delete the buffer, I thought that renaming it is going to provide a more complete illusion that the buffer is indeed deleted - any attempt to access the original buffer name is anyway erroneous, and the expected result is that it does not exist. Many plugins have unique buffer names and simply hitting "q" or a scripted open/close of those plugins is throwing an error in nvim_buf_set_name.

eyalz800 commented 1 year ago

I re-read the code and realized I'm changing the name too early, it's now better reflecting my explanation from earlier.

edit: but it does not always work, I'm checking why

eyalz800 commented 1 year ago

I probably understand less about the edge cases than you, I have multiple issues with multiple plugins just when I moved from the original solution that was changing names to the new solution that I pushed now.

When I was changing the name outside of the BufHidden, then everything worked, and then I added a change that changes the name only when BufHidden is fired but it is not fired in some cases, and I'm not sure why. The folke/edgy.nvim plugin sets the bufhidden = 'wipe' then closes the window then sets name immediately line after line, and then set name..

I'm still debugging it and not sure why I am not getting BufHidden for it.

eyalz800 commented 1 year ago

I found why the bufhidden was not being called on some edgy windows - and the reason is they use noautocmd in certain places, when removed it is indeed called

stevearc commented 1 year ago

Yeah both stickybuf and edgy do some fairly invasive magic with buffer and window handling. I wouldn't expect them to play nice together.

eyalz800 commented 1 year ago

I think we need to drop this change, this is getting too technical too quickly, so I'm closing the pull request, if you have some ideas to share please do so here

eyalz800 commented 1 year ago

@stevearc what do you think about the following way to pin? It's probably filled with edge cases, I just added in an edit the WinClosed command which I forgot initially

local m = {}
local v = vim
local augroup = v.api.nvim_create_augroup
local autocmd = v.api.nvim_create_autocmd
local clear_autocmds = v.api.nvim_clear_autocmds

m.filetyeps = {
    'qf',
    'fugitiveblame',
    'NvimTree',
    'Outline',
}

local make_win_ref = function(buf)
    local win = v.api.nvim_open_win(buf, false, {
        relative = 'editor',
        width = 1,
        height = 1,
        col = 0,
        row = 0,
        style = 'minimal',
        noautocmd = true,
    })
    v.w[win].pin_data = { buf = buf }
    return win
end

local open_in_best_win = function(buf)
    for winnr = 1, v.fn.winnr '$' do
        local winid = v.fn.win_getid(winnr)
        if not v.w[winid].pin_data and v.api.nvim_win_get_config(winid or 0).relative == '' then
            v.cmd.wincmd({ count = winnr, args = { 'w' } })
            v.cmd.buffer({ args = { buf } })
            return
        end
    end
    v.fn.win_execute(v.fn.win_getid(1), string.format('vertical rightbelow sbuffer %d', buf))
    v.cmd.wincmd({ count = 2, args = { 'w' } })
end

m.pin = function(opts)
    opts = opts or {}

    local buf = opts.buf or v.api.nvim_get_current_buf()
    local win = opts.win or v.api.nvim_get_current_win()

    local buf_pin_data = {
        win = win,
        name = v.api.nvim_buf_get_name(buf),
    }
    local win_pin_data = { buf = buf }

    v.b[buf].pin_data = buf_pin_data
    v.w[win].pin_data = win_pin_data

    local group = augroup('init.lua.pin.bufwinleave', { clear = false })
    clear_autocmds({ group = group, buffer = buf })
    autocmd('bufwinleave', {
        group = group,
        buffer = buf,
        callback = function(args)
            local pin_data = v.b[args.buf].pin_data
            if not pin_data.closing then
                pin_data.win_ref = make_win_ref(args.buf)
                v.b[args.buf].pin_data = pin_data
            end
        end,
    })

    group = augroup('init.lua.pin.winclosed', { clear = false })
    clear_autocmds({ group = group, buffer = buf })
    autocmd('winclosed', {
        group = group,
        buffer = buf,
        callback = function(args)
            local pin_data = v.b[args.buf].pin_data
            pin_data.closing = true
            v.b[args.buf].pin_data = pin_data
        end,
    })
end

m.setup = function()
    autocmd('bufenter', {
        group = augroup('init.lua.pin.bufenter', {}),
        callback = function(args)
            local win_pin_data = v.w.pin_data
            local buf = args.buf

            if win_pin_data then
                if buf == win_pin_data.buf then
                    return
                end

                local buf_pin_data = v.b[win_pin_data.buf].pin_data
                if not buf_pin_data.win_ref then
                    return
                end

                local win_ref = make_win_ref(buf)
                v.api.nvim_win_set_buf(buf_pin_data.win, win_pin_data.buf)
                v.api.nvim_win_close(buf_pin_data.win_ref, true)
                buf_pin_data.win_ref = nil

                open_in_best_win(buf)
                v.api.nvim_win_close(win_ref, true)
                return
            end

            local buf_pin_data = v.b[buf].pin_data
            if buf_pin_data then
                win_pin_data = { buf = buf }
                v.w.pin_data = win_pin_data

                buf_pin_data.win = v.api.nvim_get_current_win()
                v.b[buf].pin_data = buf_pin_data
            else
                local buf_ft = v.bo[buf].filetype
                for _, ft in ipairs(m.filetyeps) do
                    if buf_ft == ft then
                        m.pin({buf=buf, win=v.api.nvim_get_current_win()})
                        win_pin_data = { buf = buf }
                        buf_pin_data = v.b[buf].pin_data
                        break
                    end
                end
            end
        end,
    })
end

return m
eyalz800 commented 1 year ago

After some stress tests I arrived at the following

local m = {}
local v = require 'vim'
local augroup = v.api.nvim_create_augroup
local autocmd = v.api.nvim_create_autocmd
local clear_autocmds = v.api.nvim_clear_autocmds

m.filetyeps = {
    'qf',
    'fugitiveblame',
    'NvimTree',
    'Outline',
}

local make_win_ref = function(buf)
    local success, win = pcall(v.api.nvim_open_win, buf, false, {
        relative = 'editor',
        width = 1,
        height = 2,
        col = 0,
        row = 0,
        style = 'minimal',
        noautocmd = true,
    })
    if success then
        v.w[win].pin_data = { buf = buf }
        return win
    end
end

local open_in_best_win = function(buf)
    for winnr = 1, v.fn.winnr '$' do
        local winid = v.fn.win_getid(winnr)
        if not v.w[winid].pin_data and v.api.nvim_win_get_config(winid or 0).relative == '' then
            v.cmd.wincmd({ count = winnr, args = { 'w' } })
            v.cmd.buffer({ args = { buf } })
            return
        end
    end
    v.fn.win_execute(v.fn.win_getid(1), string.format('vertical rightbelow sbuffer %d', buf))
    v.cmd.wincmd({ count = 2, args = { 'w' } })
end

local is_auto_deleted = function(buf)
    local bufhidden = v.bo[buf].bufhidden
    return bufhidden == 'unload' or bufhidden == 'delete' or bufhidden == 'wipe'
end

m.pin = function(opts)
    opts = opts or {}

    local buf = opts.buf or v.api.nvim_get_current_buf()
    local win = opts.win or v.api.nvim_get_current_win()

    local buf_pin_data = {
        win = win,
        name = v.api.nvim_buf_get_name(buf),
    }
    local win_pin_data = { buf = buf }

    v.b[buf].pin_data = buf_pin_data
    v.w[win].pin_data = win_pin_data

    local group = augroup('init.lua.pin.bufwinleave', { clear = false })
    clear_autocmds({ group = group, buffer = buf })
    autocmd('bufwinleave', {
        group = group,
        buffer = buf,
        callback = function(args)
            local pin_data = v.b[args.buf].pin_data
            if not pin_data.closing then
                if is_auto_deleted(args.buf) then
                    pin_data.win_ref = make_win_ref(args.buf)
                end
                v.b[args.buf].pin_data = pin_data
            end
        end,
    })

    group = augroup('init.lua.pin.winclosed', { clear = false })
    clear_autocmds({ group = group, buffer = buf })
    autocmd('winclosed', {
        group = group,
        buffer = buf,
        callback = function(args)
            local pin_data = v.b[args.buf].pin_data
            pin_data.closing = true
            v.b[args.buf].pin_data = pin_data
        end,
    })
end

m.setup = function()
    autocmd('bufenter', {
        group = augroup('init.lua.pin.bufenter', {}),
        callback = function(args)
            local win_pin_data = v.w.pin_data
            local buf = args.buf

            if win_pin_data then
                if buf == win_pin_data.buf then
                    return
                end

                local buf_pin_data = nil
                if not v.api.nvim_buf_is_valid(win_pin_data.buf) then
                    win_pin_data = nil
                else
                    buf_pin_data = v.b[win_pin_data.buf].pin_data
                    if not buf_pin_data then
                        win_pin_data = nil
                    end
                end

                if not win_pin_data then
                    v.w.pin_data = nil
                else
                    local win_ref = nil
                    if is_auto_deleted(buf) then
                        win_ref = make_win_ref(buf)
                        if not win_ref then
                            if buf_pin_data.win_ref then
                                pcall(v.api.nvim_win_close, buf_pin_data.win_ref, true)
                                v.b[win_pin_data.buf].pin_data.win_ref = nil
                            end
                            return
                        end
                    end

                    local success, _ = pcall(v.api.nvim_win_set_buf, buf_pin_data.win, win_pin_data.buf)
                    if buf_pin_data.win_ref then
                        pcall(v.api.nvim_win_close, buf_pin_data.win_ref, true)
                        v.b[win_pin_data.buf].pin_data.win_ref = nil
                    end

                    if success then
                        pcall(open_in_best_win, buf)
                    end

                    if win_ref then
                        pcall(v.api.nvim_win_close, win_ref, true)
                    end
                    return
                end
            end

            local buf_pin_data = v.b[buf].pin_data
            if buf_pin_data then
                if not v.api.nvim_win_is_valid(buf_pin_data.win) then
                    win_pin_data = { buf = buf }
                    v.w.pin_data = win_pin_data
                    buf_pin_data.win = v.api.nvim_get_current_win()
                    v.b[buf].pin_data = buf_pin_data
                end
            else
                local buf_ft = v.bo[buf].filetype
                for _, ft in ipairs(m.filetyeps) do
                    if buf_ft == ft then
                        m.pin({buf=buf, win=v.api.nvim_get_current_win()})
                        break
                    end
                end
            end
        end,
    })
end

return m
eyalz800 commented 1 year ago

@stevearc I would really appreciate if you have any feedback on the approach taken in the post above, only if you have time to review reply

stevearc commented 1 year ago

It looks like a reasonable approach. Is there anything in particular you wanted feedback on?

eyalz800 commented 1 year ago

Mostly missed edge cases or issues it could have, so far I noticed that I miss a case if I’m on nvim-tree and I open a directory via :n directory then nvim tree gets the bufenter before there is anything there and I don’t pin it, I decided to leave as is because it’s not part of my workflow so much