feat: Major improvements in API. Particularly relating to entries.

This commit is contained in:
TJ DeVries
2020-09-03 23:56:49 -04:00
parent 737363097b
commit 839f57efb3
13 changed files with 690 additions and 216 deletions

View File

@@ -42,23 +42,32 @@ function actions.goto_file_selection(prompt_bufnr)
print("[telescope] Nothing currently selected")
return
else
local value = entry.value
if not value then
print("Could not do anything with blank line...")
return
local filename, row, col
if entry.filename then
filename = entry.filename
-- TODO: Check for off-by-one
row = entry.row or entry.lnum
col = entry.col
else
-- TODO: Might want to remove this and force people
-- to put stuff into `filename`
local value = entry.value
if not value then
print("Could not do anything with blank line...")
return
end
if type(value) == "table" then
value = entry.display
end
local sections = vim.split(value, ":")
filename = sections[1]
row = tonumber(sections[2])
col = tonumber(sections[3])
end
-- TODO: This is not great.
if type(value) == "table" then
value = entry.display
end
local sections = vim.split(value, ":")
local filename = sections[1]
local row = tonumber(sections[2])
local col = tonumber(sections[3])
vim.cmd(string.format([[bwipeout! %s]], prompt_bufnr))
a.nvim_set_current_win(picker.original_win_id or 0)

View File

@@ -11,94 +11,69 @@ This will use the default configuration options.
Other configuration options still in flux at the moment
--]]
-- TODO: Give some bonus weight to files we've picked before
-- TODO: Give some bonus weight to oldfiles
local actions = require('telescope.actions')
local finders = require('telescope.finders')
local make_entry = require('telescope.make_entry')
local previewers = require('telescope.previewers')
local pickers = require('telescope.pickers')
local sorters = require('telescope.sorters')
local utils = require('telescope.utils')
local flatten = vim.tbl_flatten
-- TODO: Support silver search here.
-- TODO: Support normal grep here (in case neither are installed).
local vimgrep_arguments = {'rg', '--color=never', '--no-heading', '--with-filename', '--line-number', '--column'}
local builtin = {}
builtin.git_files = function(opts)
opts = opts or {}
local make_entry = (
opts.shorten_path
and function(value)
local result = {
valid = true,
display = utils.path_shorten(value),
ordinal = value,
value = value
}
return result
end)
or nil
pickers.new(opts, {
prompt = 'Git File',
finder = finders.new_oneshot_job({ "git", "ls-files" }, make_entry),
finder = finders.new_oneshot_job(
{ "git", "ls-files", "-o", "--exclude-standard", "-c" },
make_entry.gen_from_file(opts)
),
previewer = previewers.cat,
sorter = sorters.get_fuzzy_file(),
}):find()
end
builtin.live_grep = function(opts)
local live_grepper = finders.new {
fn_command = function(_, prompt)
-- TODO: Make it so that we can start searching on the first character.
opts = opts or {}
local live_grepper = finders.new_job(function(prompt)
-- TODO: Probably could add some options for smart case and whatever else rg offers.
if not prompt or prompt == "" then
return nil
end
return {
command = 'rg',
args = {"--vimgrep", prompt},
}
end
}
return flatten { vimgrep_arguments, prompt }
end,
opts.entry_maker or make_entry.gen_from_vimgrep(opts),
opts.max_results
)
pickers.new(opts, {
prompt = 'Live Grep',
finder = live_grepper,
previewer = previewers.vimgrep,
}):find()
-- TODO: Incorporate this.
-- Weight the results somehow to be more likely to be the ones that you've opened.
-- local old_files = {}
-- for _, f in ipairs(vim.v.oldfiles) do
-- old_files[f] = true
-- end
-- local oldfiles_sorter = sorters.new {
-- scoring_function = function(prompt, entry)
-- local line = entry.value
-- if not line then
-- return
-- end
-- local _, finish = string.find(line, ":")
-- local filename = string.sub(line, 1, finish - 1)
-- local expanded_fname = vim.fn.fnamemodify(filename, ':p')
-- if old_files[expanded_fname] then
-- print("Found oldfiles: ", entry.value)
-- return 0
-- else
-- return 1
-- end
-- end
-- }
end
-- TODO: document_symbol
-- TODO: workspace_symbol
builtin.lsp_references = function(opts)
opts = opts or {}
opts.shorten_path = utils.get_default(opts.shorten_path, true)
local params = vim.lsp.util.make_position_params()
params.context = { includeDeclaration = true }
@@ -108,15 +83,74 @@ builtin.lsp_references = function(opts)
vim.list_extend(locations, vim.lsp.util.locations_to_items(server_results.result) or {})
end
local results = utils.quickfix_items_to_entries(locations)
if vim.tbl_isempty(results) then
if vim.tbl_isempty(locations) then
return
end
local reference_picker = pickers.new(opts, {
pickers.new(opts, {
prompt = 'LSP References',
finder = finders.new_table(results),
finder = finders.new_table {
results = locations,
entry_maker = make_entry.gen_from_quickfix(opts),
},
previewer = previewers.qflist,
sorter = sorters.get_norcalli_sorter(),
}):find()
end
builtin.lsp_document_symbols = function(opts)
local params = vim.lsp.util.make_position_params()
local results_lsp = vim.lsp.buf_request_sync(0, "textDocument/documentSymbol", params)
if not results_lsp or vim.tbl_isempty(results_lsp) then
print("No results from textDocument/documentSymbol")
return
end
local locations = {}
for _, server_results in pairs(results_lsp) do
vim.list_extend(locations, vim.lsp.util.symbols_to_items(server_results.result, 0) or {})
end
if vim.tbl_isempty(locations) then
return
end
pickers.new(opts, {
prompt = 'LSP Document Symbols',
finder = finders.new_table {
results = locations,
entry_maker = make_entry.gen_from_quickfix(opts)
},
previewer = previewers.vim_buffer,
sorter = sorters.get_norcalli_sorter(),
}):find()
end
builtin.lsp_workspace_symbols = function(opts)
local params = {query = opts.query or ''}
local results_lsp = vim.lsp.buf_request_sync(0, "workspace/symbol", params, 1000)
if not results_lsp or vim.tbl_isempty(results_lsp) then
print("No results from textDocument/documentSymbol")
return
end
local locations = {}
for _, server_results in pairs(results_lsp) do
vim.list_extend(locations, vim.lsp.util.symbols_to_items(server_results.result, 0) or {})
end
if vim.tbl_isempty(locations) then
return
end
pickers.new(opts, {
prompt = 'LSP Workspace Symbols',
finder = finders.new_table {
results = locations,
entry_maker = make_entry.gen_from_quickfix(opts)
},
previewer = previewers.qflist,
sorter = sorters.get_norcalli_sorter(),
}):find()
@@ -124,15 +158,17 @@ end
builtin.quickfix = function(opts)
local locations = vim.fn.getqflist()
local results = utils.quickfix_items_to_entries(locations)
if vim.tbl_isempty(results) then
if vim.tbl_isempty(locations) then
return
end
pickers.new(opts, {
prompt = 'Quickfix',
finder = finders.new_table(results),
finder = finders.new_table {
results = locations,
entry_maker = make_entry.gen_from_quickfix(opts),
},
previewer = previewers.qflist,
sorter = sorters.get_norcalli_sorter(),
}):find()
@@ -146,15 +182,16 @@ builtin.loclist = function(opts)
value.filename = filename
end
local results = utils.quickfix_items_to_entries(locations)
if vim.tbl_isempty(results) then
if vim.tbl_isempty(locations) then
return
end
pickers.new(opts, {
prompt = 'Loclist',
finder = finders.new_table(results),
finder = finders.new_table {
results = locations,
entry_maker = make_entry.gen_from_quickfix(opts),
},
previewer = previewers.qflist,
sorter = sorters.get_norcalli_sorter(),
}):find()
@@ -165,9 +202,12 @@ builtin.grep_string = function(opts)
local search = opts.search or vim.fn.expand("<cword>")
local file_picker = pickers.new(opts, {
pickers.new(opts, {
prompt = 'Find Word',
finder = finders.new_oneshot_job {'rg', '--vimgrep', search},
finder = finders.new_oneshot_job(
flatten { vimgrep_arguments, search},
make_entry.gen_from_vimgrep(opts)
),
previewer = previewers.vimgrep,
sorter = sorters.get_norcalli_sorter(),
}):find()
@@ -218,27 +258,32 @@ end
-- vim.fn.setreg("+", "nnoremap $TODO :lua require('telescope.builtin').<whatever>()<CR>")
-- TODO: Can we just do the names instead?
builtin.builtin = function(opts)
opts = opts or {}
opts.hide_filename = utils.get_default(opts.hide_filename, true)
opts.ignore_filename = utils.get_default(opts.ignore_filename, true)
local objs = {}
for k, v in pairs(builtin) do
local debug_info = debug.getinfo(v)
table.insert(objs, {
vimgrep_str = k,
filename = string.sub(debug_info.source, 2),
lnum = debug_info.linedefined,
col = 0,
text = k,
start = debug_info.linedefined,
finish = debug_info.lastlinedefined,
})
end
local entries = utils.quickfix_items_to_entries(objs)
pickers.new(opts, {
prompt = 'Telescope Builtin',
finder = finders.new_table(entries),
finder = finders.new_table {
results = objs,
entry_maker = make_entry.gen_from_quickfix(opts),
},
previewer = previewers.qflist,
sorter = sorters.get_norcalli_sorter(),
}):find()
@@ -269,14 +314,10 @@ builtin.fd = function(opts)
pickers.new(opts, {
prompt = 'Find Files',
finder = finders.new {
fn_command = function()
return {
command = fd_string,
cwd = cwd,
}
end,
},
finder = finders.new_oneshot_job(
{fd_string},
make_entry.gen_from_file(opts)
),
previewer = previewers.cat,
sorter = sorters.get_fuzzy_file(),
}):find()

View File

@@ -1,27 +0,0 @@
local Entry = {}
Entry.__index = Entry
-- TODO: Can we / should we make it so that "display" and "ordinal" are just values, instead of functions.
-- It seems like that's what you'd want... No need to call the functions a million times.
-- Pass in a table, that contains some state
-- Table determines it's ordinal value
function Entry:new(line_or_obj)
if type(line_or_obj) == "string" then
return setmetatable({
valid = line_or_obj ~= "",
value = line_or_obj,
ordinal = line_or_obj,
display = line_or_obj,
}, self)
else
return line_or_obj
end
end
function Entry:__tostring()
return "<" .. self.display .. ">"
end
return Entry

View File

@@ -1,5 +1,6 @@
local Job = require('plenary.job')
local make_entry = require('telescope.make_entry')
local log = require('telescope.log')
local utils = require('telescope.utils')
@@ -10,35 +11,43 @@ local finders = {}
-- FunctionFinder(my_func)
-- JobFinder(my_job_args)
---@class Finder
local Finder = {}
local _callable_obj = function()
local obj = {}
Finder.__index = Finder
Finder.__call = function(t, ... ) return t:_find(...) end
obj.__index = obj
obj.__call = function(t, ...) return t:_find(...) end
return obj
end
--[[ =============================================================
JobFinder
Uses an external Job to get results. Processes results as they arrive.
For more information about how Jobs are implemented, checkout 'plenary.job'
-- ============================================================= ]]
local JobFinder = _callable_obj()
--- Create a new finder command
---
---@param opts table Keys:
-- fn_command function The function to call
function Finder:new(opts)
function JobFinder:new(opts)
opts = opts or {}
-- TODO: Add config for:
-- - cwd
assert(not opts.results, "`results` should be used with finder.new_table")
-- TODO:
-- - `types`
-- job
-- pipe
-- vim.loop.new_pipe (stdin / stdout). stdout => filter pipe
-- rg huge_search | fzf --filter prompt_is > buffer. buffer could do stuff do w/ preview callback
-- string
-- list
-- ...
local obj = setmetatable({
results = opts.results,
entry_maker = opts.entry_maker,
entry_maker = opts.entry_maker or make_entry.from_string,
fn_command = opts.fn_command,
static = opts.static,
state = {},
@@ -51,26 +60,7 @@ function Finder:new(opts)
return obj
end
-- Probably should use the word apply here, since we're apply the callback passed to us by
-- the picker... But I'm not sure how we want to say that.
-- find_incremental
-- find_prompt
-- process_prompt
-- process_search
-- do_your_job
-- process_plz
function Finder:_find(prompt, process_result, process_complete)
if self.results then
assert(type(self.results) == 'table', "self.results must be a table")
for _, v in ipairs(self.results) do
process_result(v)
end
process_complete()
return
end
function JobFinder:_find(prompt, process_result, process_complete)
if self.job and not self.job.is_shutdown then
self.job:shutdown()
end
@@ -140,22 +130,66 @@ function Finder:_find(prompt, process_result, process_complete)
self.job:start()
end
--- Return a new Finder
--
--@return Finder
finders.new = function(opts)
return Finder:new(opts)
--[[ =============================================================
Static Finders
A static finder has results that never change.
They are passed in directly as a result.
-- ============================================================= ]]
local StaticFinder = _callable_obj()
function StaticFinder:new(opts)
assert(opts, "Options are required. See documentation for usage")
local input_results
if vim.tbl_islist(opts) then
input_results = opts
else
input_results = opts.results
end
local entry_maker = opts.entry_maker or make_entry.gen_from_string()
assert(input_results)
assert(input_results, "Results are required for static finder")
assert(type(input_results) == 'table', "self.results must be a table")
local results = {}
for _, v in ipairs(input_results) do
table.insert(results, entry_maker(v))
end
return setmetatable({ results = results }, self)
end
-- TODO: Is this worth making?
-- finders.new_responsive_job = function(opts)
-- return finders.new {
-- maximum_results = get_default(opts.maximum_results, 2000),
-- }
-- end
function StaticFinder:_find(_, process_result, process_complete)
for _, v in ipairs(self.results) do
process_result(v)
end
finders.new_job = function(command_generator)
return finders.new {
process_complete()
end
-- local
--- Return a new Finder
--
-- Use at your own risk.
-- This opts dictionary is likely to change, but you are welcome to use it right now.
-- I will try not to change it needlessly, but I will change it sometimes and I won't feel bad.
finders._new = function(opts)
if opts.results then
print("finder.new is deprecated with `results`. You should use `finder.new_table`")
return StaticFinder:new(opts)
end
return JobFinder:new(opts)
end
finders.new_job = function(command_generator, entry_maker, maximum_results)
return JobFinder:new {
fn_command = function(_, prompt)
local command_list = command_generator(prompt)
if command_list == nil then
@@ -168,19 +202,24 @@ finders.new_job = function(command_generator)
command = command,
args = command_list,
}
end
end,
entry_maker = entry_maker,
maximum_results = maximum_results,
}
end
---@param command_list string[] Command list to execute.
---@param entry_maker function Optional: function(line: string) => table
finders.new_oneshot_job = function(command_list, entry_maker)
command_list = vim.deepcopy(command_list)
local command = table.remove(command_list, 1)
return finders.new {
return JobFinder:new {
static = true,
entry_maker = entry_maker,
entry_maker = entry_maker or make_entry.from_string,
fn_command = function()
return {
@@ -191,18 +230,14 @@ finders.new_oneshot_job = function(command_list, entry_maker)
}
end
--- Used to create a finder for a Lua table.
-- If you only pass a table of results, then it will use that as the entries.
--
-- If you pass a table, and then a function, it's used as:
-- results table, the results to run on
-- entry_maker function, the function to convert results to entries.
finders.new_table = function(t)
return finders.new {
results = t
}
return StaticFinder:new(t)
end
-- We should add a few utility functions here...
--
-- finders.new_job
-- finders.new_one_shot_job
-- finders.new_table
-- finders.Finder = Finder
return finders

View File

@@ -0,0 +1,162 @@
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
local utils = require('telescope.utils')
local make_entry = {}
make_entry.types = {
GENERIC = 0,
FILE = 1,
}
local transform_devicons
if has_devicons then
transform_devicons = function(filename, display, opts)
if opts.disable_devicons then
return display
end
return (devicons.get_icon(filename, string.match(filename, '%a+$')) or ' ') .. ' ' .. display
end
else
transform_devicons = function(_, display, _)
return display
end
end
function make_entry.gen_from_string()
return function(line)
return {
valid = line ~= "",
entry_type = make_entry.types.SIMPLE,
value = line,
ordinal = line,
display = line,
}
end
end
function make_entry.gen_from_file(opts)
opts = opts or {}
local make_display = function(line)
local display = line
if opts.shorten_path then
display = utils.path_shorten(line)
end
display = transform_devicons(line, display, opts)
return display
end
return function(line)
local entry = {
ordinal = line,
value = line,
entry_type = make_entry.types.FILE,
filename = line,
}
entry.display = make_display(line)
return entry
end
end
function make_entry.gen_from_vimgrep(opts)
opts = opts or {}
local display_string = "%s:%s%s"
local make_display = function(entry)
local display = entry.value
local display_filename
if opts.shorten_path then
display_filename = utils.path_shorten(entry.filename)
else
display_filename = entry.filename
end
local coordinates = ""
if not opts.disable_coordinates then
coordinates = string.format("%s:%s:", entry.lnum, entry.col)
end
display = transform_devicons(
entry.filename,
string.format(display_string, display_filename, coordinates, entry.text),
opts
)
return display
end
return function(line)
-- TODO: Consider waiting to do this string.find
-- TODO: Is this the fastest way to get each of these?
-- Or could we just walk the text and check for colons faster?
local _, _, filename, lnum, col, text = string.find(line, [[([^:]+):(%d+):(%d+):(.*)]])
return {
valid = line ~= "",
value = line,
ordinal = line,
display = make_display,
entry_type = make_entry.types.FILE,
filename = filename,
lnum = lnum,
col = col,
text = text,
}
end
end
function make_entry.gen_from_quickfix(opts)
opts = opts or {}
local make_display = function(entry)
local to_concat = {}
if not opts.hide_filename then
local filename = entry.filename
if opts.shorten_path then
filename = utils.path_shorten(filename)
end
table.insert(to_concat, filename)
table.insert(to_concat, ":")
end
table.insert(to_concat, entry.text)
return table.concat(to_concat, "")
end
return function(entry)
return {
valid = true,
value = entry,
ordinal = (
not opts.ignore_filename and entry.filename
or ''
) .. ' ' .. entry.text,
display = make_display,
filename = entry.filename,
lnum = entry.lnum,
col = entry.col,
text = entry.text,
start = entry.start,
finish = entry.finish,
}
end
end
return make_entry

View File

@@ -1,6 +1,5 @@
local a = vim.api
local popup = require('popup')
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
local actions = require('telescope.actions')
local log = require('telescope.log')
@@ -8,8 +7,6 @@ local mappings = require('telescope.mappings')
local state = require('telescope.state')
local utils = require('telescope.utils')
local Entry = require('telescope.entry')
local get_default = utils.get_default
-- TODO: Make this work with deep extend I think.
@@ -255,14 +252,19 @@ function Picker:find()
return
end
-- TODO: This really isn't the place to do this.
local display = entry.display
if has_devicons then
local icon = devicons.get_icon(display, vim.fn.fnamemodify(display, ":e"))
display = (icon or ' ') .. ' ' .. display
local display
if type(entry.display) == 'function' then
display = entry:display()
elseif type(entry.display) == 'string' then
display = entry.display
else
log.info("Weird entry", entry)
return
end
-- This is the two spaces to manage the '> ' stuff.
-- Maybe someday we can use extmarks or floaty text or something to draw this and not insert here.
-- until then, insert two spaces
display = ' ' .. display
-- log.info("Setting row", row, "with value", entry)
@@ -277,10 +279,9 @@ function Picker:find()
end
))
local process_result = function(line)
local entry = Entry:new(line)
if not entry.valid then
local process_result = function(entry)
-- TODO: Should we even have valid?
if entry.valid == false then
return
end
@@ -559,10 +560,7 @@ pickers.entry_manager = function(max_results, set_entry)
return setmetatable({
add_entry = function(self, score, entry)
-- TODO: Consider forcing people to make these entries before we add them.
if type(entry) == "string" then
entry = Entry:new(entry)
end
assert(type(entry) == "table", "entry must be a table by the time it reaches here")
score = score or 0
@@ -647,6 +645,10 @@ function pickers.on_close_prompt(prompt_bufnr)
local picker = status.picker
picker:close_windows(status)
if picker.previewer then
picker.previewer:teardown()
end
end

View File

@@ -8,6 +8,7 @@ local Previewer = {}
Previewer.__index = Previewer
local bat_options = " --style=grid --paging=always "
local previewer_ns = vim.api.nvim_create_namespace('telescope.previewers')
-- --terminal-width=%s
-- TODO: We shoudl make sure that all our terminals close all the way.
@@ -20,6 +21,7 @@ function Previewer:new(opts)
return setmetatable({
state = nil,
_setup_func = opts.setup,
_teardown_func = opts.teardown,
preview_fn = opts.preview_fn,
}, Previewer)
end
@@ -29,13 +31,22 @@ function Previewer:preview(entry, status)
return
end
if not self.state and self._setup_func then
self.state = self._setup_func()
if not self.state then
if self._setup_func then
self.state = self._setup_func()
end
end
self:teardown()
return self:preview_fn(entry, status)
end
function Previewer:teardown()
if self._teardown_func then
self:_teardown_func()
end
end
previewers.new = function(...)
return Previewer:new(...)
end
@@ -69,20 +80,36 @@ previewers.new_termopen = function(opts)
end
previewers.vim_buffer = previewers.new {
preview_fn = function(_, entry, status)
local value = entry.value
if value == nil then
setup = function() return { last_set_bufnr = nil } end,
teardown = function(self)
if self.state.last_set_bufnr then
vim.api.nvim_buf_clear_namespace(self.state.last_set_bufnr, previewer_ns, 0, -1)
end
end,
preview_fn = function(self, entry, status)
local filename = entry.filename
if filename == nil then
local value = entry.value
filename = vim.split(value, ":")[1]
end
if filename == nil then
return
end
local file_name = vim.split(value, ":")[1]
log.trace("Previewing File: %s", file_name)
log.trace("Previewing File: %s", filename)
-- vim.fn.termopen(
-- string.format("bat --color=always --style=grid %s"),
-- vim.fn.fnamemodify(file_name, ":p")
local bufnr = vim.fn.bufadd(file_name)
vim.fn.bufload(bufnr)
local bufnr = vim.fn.bufnr(filename)
if bufnr == -1 then
-- TODO: Is this the best way to load the buffer?... I'm not sure tbh
bufnr = vim.fn.bufadd(bufnr)
vim.fn.bufload(bufnr)
end
self.state.last_set_bufnr = bufnr
-- TODO: We should probably call something like this because we're not always getting highlight and all that stuff.
-- api.nvim_command('doautocmd filetypedetect BufRead ' .. vim.fn.fnameescape(filename))
@@ -92,6 +119,11 @@ previewers.vim_buffer = previewers.new {
-- vim.api.nvim_win_set_option(preview_win, 'winblend', 20)
vim.api.nvim_win_set_option(status.preview_win, 'signcolumn', 'no')
vim.api.nvim_win_set_option(status.preview_win, 'foldlevel', 100)
if entry.lnum then
vim.api.nvim_buf_add_highlight(bufnr, previewer_ns, "Visual", entry.lnum - 1, 0, -1)
-- print("LNUM:", entry.lnum)
end
end,
}

View File

@@ -99,6 +99,10 @@ utils.path_shorten = (function()
]]
return function(path)
if not path then
return path
end
local c_str = ffi.new("char[?]", #path + 1)
ffi.copy(c_str, path)
return ffi.string(ffi.C.shorten_dir(c_str))

View File

@@ -1,15 +1,21 @@
RELOAD('telescope')
require('plenary.reload').reload_module('telescope')
local actions = require('telescope.actions')
local finders = require('telescope.finders')
local previewers = require('telescope.previewers')
local pickers = require('telescope.pickers')
local sorters = require('telescope.sorters')
local utils = require('telescope.utils')
pickers.new({
prompt = 'Telescope Builtin',
finder = finders.new_table({"hello\nworld", "other", "item"}),
finder = finders.new_table {
results = {"hello\nworld", "other", "item"},
entry_maker = false and function(line)
return {
value = line,
ordinal = line,
display = "wow: // " .. line,
}
end,
},
sorter = sorters.get_norcalli_sorter(),
}):find()