nvim-telescope / telescope.nvim

Find, Filter, Preview, Pick. All lua, all the time.
MIT License
15.96k stars 838 forks source link

Sort recently opened files to the top of `find_files` #2109

Closed bengolds closed 9 months ago

bengolds commented 2 years ago

When I do a file search, it'd be great if information on my recently opened files was included in order to: 1) Speed up the file fuzzy search 2) Sort recently opened files to the top.

I believe this is the default behavior of the VSCode file picker.

I can't figure out how to do this, so I'm not sure if I'm missing something -- do I need to combine picker sources? fzf-frecency seems to sort of do this, but is deadly slow when you try to include the files in the current workspace.

trickstival commented 2 years ago

Not sure if it helps, but there is a plugin called telescope-frecency: https://github.com/nvim-telescope/telescope-frecency.nvim

iwfan commented 2 years ago

Ohhhhhh, I also want to show off oldfiles first when I use find_files, It's really convenience for quick navigation to recent used files.

I found a hidden API named on_input_filter_cb in telescope's source code. The on_input_filter_cb accepts a query string parameter named prompt. So you can do some logic in this callback function. When prompt is empty, you can use oldfiles's results. When prompt is not empty, you can use find_files's results. You can also find a use case in @tjdevries 's repo.

The draft code is as follows, But it is not complete, You can find the missing functionality in telescope's source code.

local pickers = require "telescope.pickers"
local finders = require "telescope.finders"
local make_entry = require "telescope.make_entry"

local enhance_find_files = function(opts)
    local results = {}  -- telescope oldfiles results
    pickers
        .new(opts, {
            prompt_title = "OOOOOOOO",
            finder = finders.new_table {
                results = results,
                entry_maker = opts.entry_maker or make_entry.gen_from_file(opts),
            },
            sorter = conf.file_sorter(opts),
            previewer = conf.file_previewer(opts),
            on_input_filter_cb = function(prompt)
                local is_empty = prompt == nil or prompt == ""

                if is_empty then
                    return {
                        prompt = prompt,
                        updated_finder = finders.new_table {
                            results = results,
                            entry_maker = opts.entry_maker or make_entry.gen_from_file(opts),
                        },
                    }
                end

                --  use telescope built-in find_files.
                return {
                    prompt = prompt,
                    updated_finder = finders.new_oneshot_job(find_command, opts),
                }
            end,
        })
        :find()
end

I simply copied Telescope's source code to make an extension and that works pretty well. Hope this works for you.

bengolds commented 2 years ago

@trickstival -- sadly, that extension doesn't quite work. Its fallback to searching the whole directory is super slow right now.

@iwfan -- I'll give that a try -- thanks for sharing the code!

Icelk commented 1 year ago

See this comment in the ripgrep repo for info on how to sort the results correctly: https://github.com/BurntSushi/ripgrep/issues/2243#issuecomment-1622559768

Using that ripgrep patch, I got exactly the expected results with good performance. If you want to preserve the order when searching, try a non-fuzzy sorter.

Icelk commented 1 year ago

Using the sorter function defined here, you get priority for recent files.

abdennourzahaf commented 1 year ago

@bengolds Have you found a way to do this?

gbroques commented 1 year ago

Hi all. I cobbled together a custom file sorter for find_files based on Telescope's default fzy sorter that simply doubles the score if the file is open (i.e. matches an open buffer).

It seems to work a little like VS Code's file picker.

Let me know your thoughts. I'm sure it could be improved. :)

https://github.com/gbroques/neovim-configuration/commit/e8a434d6ba0f97205517873fe3f224d14f893154

local telescope = require('telescope')
local sorters = require('telescope.sorters')
local fzy_sorter = sorters.get_fzy_sorter()
local filter = vim.tbl_filter

-- Copied from:
-- https://github.com/nvim-telescope/telescope.nvim/blob/dc192faceb2db64231ead71539761e055df66d73/lua/telescope/builtin/__internal.lua#L17-L29
local function apply_cwd_only_aliases(opts)
  local has_cwd_only = opts.cwd_only ~= nil
  local has_only_cwd = opts.only_cwd ~= nil

  if has_only_cwd and not has_cwd_only then
    -- Internally, use cwd_only
    opts.cwd_only = opts.only_cwd
    opts.only_cwd = nil
  end

  return opts
end
-- Copied from:
-- https://github.com/nvim-telescope/telescope.nvim/blob/dc192faceb2db64231ead71539761e055df66d73/lua/telescope/builtin/__internal.lua#L872-L923
local get_buffers = function(opts)
  opts = opts or {}
  opts = apply_cwd_only_aliases(opts)
  local bufnrs = filter(function(b)
    if 1 ~= vim.fn.buflisted(b) then
      return false
    end
    -- only hide unloaded buffers if opts.show_all_buffers is false, keep them listed if true or nil
    if opts.show_all_buffers == false and not vim.api.nvim_buf_is_loaded(b) then
      return false
    end
    if opts.ignore_current_buffer and b == vim.api.nvim_get_current_buf() then
      return false
    end
    if opts.cwd_only and not string.find(vim.api.nvim_buf_get_name(b), vim.loop.cwd(), 1, true) then
      return false
    end
    if not opts.cwd_only and opts.cwd and not string.find(vim.api.nvim_buf_get_name(b), opts.cwd, 1, true) then
      return false
    end
    return true
  end, vim.api.nvim_list_bufs())
  if not next(bufnrs) then
    return
  end
  if opts.sort_mru then
    table.sort(bufnrs, function(a, b)
      return vim.fn.getbufinfo(a)[1].lastused > vim.fn.getbufinfo(b)[1].lastused
    end)
  end

  local buffers = {}
  for _, bufnr in ipairs(bufnrs) do
    local flag = bufnr == vim.fn.bufnr "" and "%" or (bufnr == vim.fn.bufnr "#" and "#" or " ")

    local element = {
      bufnr = bufnr,
      flag = flag,
      info = vim.fn.getbufinfo(bufnr)[1],
    }

    if opts.sort_lastused and (flag == "#" or flag == "%") then
      local idx = ((buffers[1] ~= nil and buffers[1].flag == "%") and 2 or 1)
      table.insert(buffers, idx, element)
    else
      table.insert(buffers, element)
    end
  end
  return buffers
end

local is_file_open = function(line)
  local buffers = get_buffers()
  if not buffers then
    return false
  end
  -- TODO: This may not be performant if there are many open buffers.
  -- We could implement a map / lookup table instead.
  for _, buffer in ipairs(buffers) do
    local buffer_name = buffer.info.name
    if vim.endswith(buffer_name, line) then
      return true
    end
  end
  return false
end

-- Copied from:
-- https://github.com/nvim-telescope/telescope.nvim/blob/dc192faceb2db64231ead71539761e055df66d73/lua/telescope/sorters.lua#L437-L466
-- Sorter using the fzy algorithm
local file_sorter = function(opts)
  opts = opts or {}
  local fzy = opts.fzy_mod or require "telescope.algos.fzy"
  local OFFSET = -fzy.get_score_floor()

  return sorters.Sorter:new {
    discard = fzy_sorter.discard,

    scoring_function = function(_, prompt, line)
      -- Check for actual matches before running the scoring alogrithm.
      if not fzy.has_match(prompt, line) then
        return -1
      end

      local fzy_score = fzy.score(prompt, line)

      -- The fzy score is -inf for empty queries and overlong strings.  Since
      -- this function converts all scores into the range (0, 1), we can
      -- convert these to 1 as a suitable "worst score" value.
      if fzy_score == fzy.get_score_min() then
        return 1
      end

      -- CUSTOM CODE ADDED HERE 👇
      -- Double score if file is open.
      -- TODO: Score boost could take into account sort order of buffers.
      -- Like which one was last used.
      if is_file_open(line) then
        fzy_score = fzy_score * 2
      end
      -- END CUSTOM CODE

      -- Poor non-empty matches can also have negative values. Offset the score
      -- so that all values are positive, then invert to match the
      -- telescope.Sorter "smaller is better" convention. Note that for exact
      -- matches, fzy returns +inf, which when inverted becomes 0.
      return 1 / (fzy_score + OFFSET)
    end,

    highlighter = fzy_sorter.highlighter
  }
end
telescope.setup {
  defaults = {
    file_sorter = file_sorter,
    -- config omitted for brevity ...
  },
  -- config omitted for brevity ...
}
xzbdmw commented 9 months ago

This is what you are looking for https://github.com/danielfalk/smart-open.nvim

undg commented 9 months ago

This is what you are looking for https://github.com/danielfalk/smart-open.nvim

Just started playing with this plugin and it's really good.

jamestrew commented 9 months ago

I'm going to mark this as closed. I don't think there's any intention to add this to telescope core especially considering there are a few telescope extensions tackling this.

mollerhoj commented 8 months ago

Hi, I made an extension to solve this. Might document it later. https://github.com/mollerhoj/telescope-recent-files.nvim

Install with

  {
    'nvim-telescope/telescope.nvim',
    tag = '0.1.5',
    dependencies = {
      'mollerhoj/telescope-recent-files.nvim',
    },
    config = function()
      require("telescope").load_extension("recent-files")
    end
  },

-- A keymap
vim.keymap.set('n', '<leader>f', function()
  require('telescope').extensions['recent-files'].recent_files({})
end, { noremap = true, silent = true })