Skip to content

[mini.extra] pickers.buf_lines should go to first matched character #2384

@antonk52

Description

@antonk52

Contributing guidelines

Module(s)

mini.extra

Description

Hi and thank you for all your work on mini.nvim and neovim!

I use the buf_lines picker extensively and it is likely my most used picker. The current behavior is selecting a line the cursor goes to the beginning of that line. This means that extra steps are needed to navigate to the thing you search for.

I'd like to advocate that when searching for something the intent is most likely to navigate the very thing the user searches for. Thus navigating to the first matched character is preferred. Today this can be achieved by overriding the source.choose like require('mini.extra').pickers.buf_lines({scope = 'current'}, {source = {choose = my_impl}}), you can see the implementation in just over 100 lines below.

implementation
local query_is_ignorecase = function(query)
    if not vim.o.ignorecase then
        return false
    elseif not vim.o.smartcase then
        return true
    end

    local prompt = table.concat(query)
    return prompt == prompt:lower()
end

local match_query_group = function(query)
    local parts = { {} }
    for _, x in ipairs(query) do
        local is_whitespace = x:find('^%s+$') ~= nil
        if is_whitespace then
            table.insert(parts, {})
        else
            table.insert(parts[#parts], x)
        end
    end

    return #parts > 1, vim.tbl_map(table.concat, parts)
end

local match_find_query = function(s, query, init)
    local first, to = string.find(s, query[1], init, true)
    if first == nil then
        return nil, nil
    end

    local last = first --[[@as number?]]
    for i = 2, #query do
        last, to = string.find(s, query[i], to + 1, true)
        if not last then
            return nil, nil
        end
    end

    return first, last
end

local buf_lines_match_col = function(line, query)
    if type(line) ~= 'string' or #query == 0 then
        return nil
    elseif query_is_ignorecase(query) then
        line = line:lower()
        query = vim.tbl_map(string.lower, query)
    end

    local is_fuzzy_forced = query[1] == '*'
    local is_exact_plain = query[1] == "'"
    local is_exact_start = query[1] == '^'
    local is_exact_end = query[#query] == '$'
    local is_grouped, grouped_parts = match_query_group(query)

    if is_fuzzy_forced or is_exact_plain or is_exact_start or is_exact_end then
        local start_offset = (is_fuzzy_forced or is_exact_plain or is_exact_start) and 2 or 1
        local end_offset = #query
            - ((not is_fuzzy_forced and not is_exact_plain and is_exact_end) and 1 or 0)
        query = vim.list_slice(query, start_offset, end_offset)
    elseif is_grouped then
        query = grouped_parts
    end

    if #query == 0 then
        return nil
    end

    local is_fuzzy_plain = not (is_exact_plain or is_exact_start or is_exact_end) and #query > 1
    if is_fuzzy_forced or is_fuzzy_plain then
        local first, last = match_find_query(line, query, 1)
        if first == nil then
            return nil
        elseif first == last then
            return first
        end

        local best_first, _best_last, best_width = first, last, last - first
        while last do
            local width = last - first
            if width < best_width then
                best_first, _best_last, best_width = first, last, width
            end
            first, last = match_find_query(line, query, first + 1)
        end

        return best_first
    end

    local prefix = is_exact_start and '^' or ''
    local suffix = is_exact_end and '$' or ''
    local pattern = prefix .. vim.pesc(table.concat(query)) .. suffix
    return string.find(line, pattern)
end

local choose_buf_line_at_match = function(item)
    local query = require('mini.pick').get_picker_query() or {}
    local line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1]
    local col = buf_lines_match_col(line, query)

    if col then
        item.col = col
    end

    return require('mini.pick').default_choose(item)
end

keymap.set('n', '<leader>/', function()
    require('mini.extra').pickers.buf_lines(
        { scope = 'current' },
        { source = { choose = choose_buf_line_at_match } }
    )
end)

I think it would be great if the buf_lines picker came with this feature by default or perhaps behind an option. Can this be considered?

Aside, both telescope and snacks.picker default behavior takes the user to the first matched character.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions