camspiers / snap

A fast finder system for neovim.
The Unlicense
485 stars 16 forks source link

Sort buffers by most recently selected/used #43

Open gegoune opened 3 years ago

gegoune commented 3 years ago

FZF does sort buffers on buffers selection list by most recently used. This is very helpful and makes switching to most recently used buffers easier as they are closer to the end of the list (I do not mean oldfiles here, which is different).

FZF.vim also does not allow to pick up buffer shown in 'current' window, this is shown as yellow on screenshot below and is not selectable.

Screenshot 2021-06-23 at 10 42 58

After changing buffer to _neogit.lua and invoking fzf buffers again it changes to:

Screenshot 2021-06-23 at 10 43 52

Do you think this functionality could be implemented in snap's buffer producer?

TyberiusPrime commented 3 years ago

I'd love to have both these changes.

camspiers commented 3 years ago

All good ideas :) when I get some time I can come back to this. Thanks!

roginfarrer commented 3 years ago

I'm sure there's cleaner way to do this, but if you just want to throw something into your config for now, I was able to make a custom producer that sorts the list. Inspired by how FZF sorts their buffer list.

-- Taken from source
-- https://github.com/camspiers/snap/blob/4ed8f920f437138b7da38d5ad9003a1e2ca2ddb3/lua/snap/producer/vim/buffer.lua
local function get_buffers()
  local function _1_(_241)
    return vim.fn.bufname(_241)
  end
  local function _2_(_241)
    return ((vim.fn.bufname(_241) ~= "") and (vim.fn.buflisted(_241) == 1) and (vim.fn.bufexists(_241) == 1))
  end
  return vim.tbl_map(_1_, vim.tbl_filter(_2_, vim.api.nvim_list_bufs()))
end

-- Table of buffers
local snap_buffers = {}

function _G.push_buffer()
  local bufname = vim.fn.bufname("")
  -- push a key/value of buffer path and time entered
  snap_buffers[bufname] = vim.fn.reltimefloat(vim.fn.reltime())
end

function _G.delete_buffer()
  local bufname = vim.fn.bufname("")
  -- Remove path from table
  snap_buffers[bufname] = nil
end

vim.cmd [[
  augroup snap_buffers
    autocmd!
    autocmd BufWinEnter,WinEnter * call v:lua.push_buffer()
    autocmd BufDelete * silent! call v:lua.delete_buffer()
  augroup END
]]

local function getKeysSortedByValue(tbl)
  local keys = {}
  for key in pairs(tbl) do
    table.insert(keys, key)
  end
  table.sort(
    keys,
    function(a, b)
      return tbl[a] > tbl[b]
    end
  )
  return keys
end

local function tableHasValue(tbl, val)
  for _, value in pairs(tbl) do
    if value == val then
      return true
    end
  end
  return false
end

local function makeBufferList()
  -- Get the active buffers that snap uses
  -- (this is a trimmed down list)
  local activeBuffers = get_buffers()
  -- Get our sorted list of buffers (with some we don't want)
  local filePathsSortedByTime = getKeysSortedByValue(snap_buffers)

  local result = {}
  for _, filepath in pairs(filePathsSortedByTime) do
    if (tableHasValue(activeBuffers, filepath)) then
      -- filter down our sorted list to the same items that Snap uses
      table.insert(result, filepath)
    end
  end

  return result
end

local function bufferProducer()
  -- Runs the slow-mode to get the buffers
  local result = snap.sync(makeBufferList)
  coroutine.yield(result)
end
TyberiusPrime commented 3 years ago

This is 95% of a solution for me, thank you very much.

Only change I did for myself is to add the newest entry at the bottom:

local function makeBufferList()
  -- Get the active buffers that snap uses
  -- (this is a trimmed down list)
  local activeBuffers = get_buffers()
  -- Get our sorted list of buffers (with some we don't want)
  local filePathsSortedByTime = getKeysSortedByValue(snap_buffers)

  local result = {}
  local first = false
  for _, filepath in pairs(filePathsSortedByTime) do
    if (tableHasValue(activeBuffers, filepath)) then
      -- filter down our sorted list to the same items that Snap uses
      if first == false then
          first = filepath
      else 
          table.insert(result, filepath)
      end
    end
  end
  if first ~= false then
      table.insert(result, first)
  end
  return result
end

I find it nice not to have it on the top - I'm unlikely to not switch buffers :).

TyberiusPrime commented 3 years ago

Almost perfect, it seems to keep one buffer list per cwd :(. I don't get it.

roginfarrer commented 3 years ago

This might be a slightly more elegant way to do it. The :buffers t command will print the buffer list in order of access for us. Unfortunately I don't think there's a way to get this list programmatically, but you can do some string manipulation to create this list. I think this will be more reliable and accurate than my previous solution.

local function getSortedBufferList()
  -- Hacky way to get the list of buffers sorted by recency
  -- There's no programmatic way to get this AFAIK
  -- But the ":buffers t" command does sort it correctly
  -- This redirects the output of the command to a register,
  -- then assigns the contents of that register to a global variable
  -- we can access
  vim.cmd [[ 
    let temp_reg = @"
    redir @"
    execute "buffers t"
    redir END
    let output = copy(@")
    let g:raw_buffer_list = output
    let @" = temp_reg
  ]]

  -- This is now a multiline string of the buffer list
  -- We need to trim this down into a table of filepaths
  local rawBuffers = vim.g.raw_buffer_list
  local splitLines = vim.split(rawBuffers, "\n", false)
  local result = {}

  -- The filepaths always start at this index
  local quoteStart = 11

  for _, line in pairs(splitLines) do
    -- Identify the index position of the closing quote
    local endIndex = string.find(line, '"', quoteStart)
    if type(endIndex) == "number" then
      -- grab the string between the quotes
      local filename = string.sub(line, quoteStart, endIndex - 1)
      table.insert(result, filename)
    end
  end

  return result
end

local function bufferProducer(request)
  -- Runs the slow-mode to get the buffers
  local result = snap.sync(getSortedBufferList)

  -- Move the active buffer (first filepath)
  -- to the bottom of the list
  local currentBuffer = result[1]
  table.remove(result, 1)
  table.insert(result, currentBuffer)

  if request.canceled() then
    coroutine.yield(nil)
  else
    coroutine.yield(result)
  end
end
beardedsakimonkey commented 3 years ago

Here's a producer that sorts buffers by lastused and excludes the current buffer

(fn get-sorted-buffers [request]
  (fn []
    (let [original-buf (vim.api.nvim_win_get_buf (. request :winnr))
          bufs (vim.tbl_filter #(and (not= (vim.fn.bufname $1) "")
                                     (= (vim.fn.buflisted $1) 1)
                                     (= (vim.fn.bufexists $1) 1)
                                     (not= $1 original-buf))
                               (vim.api.nvim_list_bufs))]
      (table.sort bufs
                  #(> (. (vim.fn.getbufinfo $1) 1 :lastused)
                      (. (vim.fn.getbufinfo $2) 1 :lastused)))
      (vim.tbl_map #(vim.fn.bufname $1) bufs))))

(fn sorted-buffers [request]
  (snap.sync (get-sorted-buffers request)))

As a side note, I think the winnr field in Request might be misnamed -- it's actually the window ID. (:h winid)