From 7e9f38a87e1dfc5226665e9602e39a900519c732 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Thu, 27 Aug 2020 22:12:44 -0400 Subject: [PATCH] feat: Add livegrep and lsp referecnes --- README.md | 2 +- lua/telescope/builtin.lua | 132 +++++++++++++++++++++++++++-- lua/telescope/finders.lua | 72 ++++++++++++---- lua/telescope/init.lua | 21 ++--- lua/telescope/mappings.lua | 15 ++-- lua/telescope/pickers.lua | 157 ++++++++++++++++++++--------------- lua/telescope/previewers.lua | 150 ++++++++++++++++++++++++++++----- lua/telescope/sorters.lua | 11 ++- lua/telescope/state.lua | 6 +- lua/tests/telescope_spec.lua | 75 +++++++++++------ 10 files changed, 475 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 0c66cc8..be3209a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Plug 'nvim-lua/telescope.nvim' There is currently a fuzzy finder for git files builtin: ``` -require('telescope').builtin.git_files() +require('telescope.builtin').git_files() ``` ## Goals diff --git a/lua/telescope/builtin.lua b/lua/telescope/builtin.lua index ff5a16d..fad98d1 100644 --- a/lua/telescope/builtin.lua +++ b/lua/telescope/builtin.lua @@ -4,29 +4,37 @@ A collection of builtin pipelines for telesceope. Meant for both example and for easy startup. --]] +local finders = require('telescope.finders') +local previewers = require('telescope.previewers') +local pickers = require('telescope.pickers') +local sorters = require('telescope.sorters') + local builtin = {} -builtin.git_files = function(_) +builtin.git_files = function() -- TODO: Auto select bottom row -- TODO: filter out results when they don't match at all anymore. - local telescope = require('telescope') - - local file_finder = telescope.finders.new { + local file_finder = finders.new { static = true, - fn_command = function() return 'git ls-files' end, + fn_command = function(self) + return { + command = 'git', + args = {'ls-files'} + } + end, } - local file_previewer = telescope.previewers.vim_buffer + local file_previewer = previewers.cat - local file_picker = telescope.pickers.new { + local file_picker = pickers.new { previewer = file_previewer } -- local file_sorter = telescope.sorters.get_ngram_sorter() -- local file_sorter = require('telescope.sorters').get_levenshtein_sorter() - local file_sorter = telescope.sorters.get_norcalli_sorter() + local file_sorter = sorters.get_norcalli_sorter() file_picker:find { prompt = 'Simple File', @@ -35,5 +43,113 @@ builtin.git_files = function(_) } end +builtin.live_grep = function() + local live_grepper = finders.new { + maximum_results = 1000, + + fn_command = function(self, prompt) + -- TODO: Make it so that we can start searching on the first character. + if not prompt or prompt == "" then + return nil + end + + return { + command = 'rg', + args = {"--vimgrep", prompt}, + } + end + } + + local file_previewer = previewers.vimgrep + local file_picker = pickers.new { + previewer = file_previewer + } + + -- local file_sorter = telescope.sorters.get_ngram_sorter() + -- local file_sorter = require('telescope.sorters').get_levenshtein_sorter() + -- local file_sorter = sorters.get_norcalli_sorter() + + -- 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 + -- } + + file_picker:find { + prompt = 'Live Grep', + finder = live_grepper, + sorter = oldfiles_sorter, + } +end + +builtin.lsp_references = function() + local params = vim.lsp.util.make_position_params() + params.context = { includeDeclaration = true } + + local results_lsp = vim.lsp.buf_request_sync(0, "textDocument/references", params) + local locations = {} + for _, server_results in pairs(results_lsp) do + vim.list_extend(locations, vim.lsp.util.locations_to_items(server_results.result) or {}) + end + + local results = {} + for _, entry in ipairs(locations) do + local vimgrep_str = string.format( + "%s:%s:%s: %s", + vim.fn.fnamemodify(entry.filename, ":."), + entry.lnum, + entry.col, + entry.text + ) + + table.insert(results, { + valid = true, + value = entry, + ordinal = vimgrep_str, + display = vimgrep_str, + }) + end + + if vim.tbl_isempty(results) then + return + end + + local lsp_reference_finder = finders.new { + results = results + } + + local reference_previewer = previewers.qflist + local reference_picker = pickers.new { + previewer = reference_previewer + } + + reference_picker:find { + prompt = 'LSP References', + finder = lsp_reference_finder, + sorter = sorters.get_norcalli_sorter(), + } +end + + return builtin diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index 10716d0..3ac548c 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -1,8 +1,15 @@ -local a = vim.api +local Job = require('plenary.job') + local log = require('telescope.log') local finders = {} + +-- TODO: We should make a few different "FinderGenerators": +-- SimpleListFinder(my_list) +-- FunctionFinder(my_func) +-- JobFinder(my_job_args) + ---@class Finder local Finder = {} @@ -30,10 +37,14 @@ function Finder:new(opts) -- ... return setmetatable({ results = opts.results, + fn_command = opts.fn_command, static = opts.static, state = {}, - job_id = -1, + + -- Maximum number of results to process. + -- Particularly useful for live updating large queries. + maximum_results = opts.maximum_results, }, Finder) end @@ -57,8 +68,8 @@ function Finder:_find(prompt, process_result, process_complete) return end - if (self.state.job_id or 0) > 0 then - vim.fn.jobstop(self.job_id) + if self.job and not self.job.is_shutdown then + self.job:shutdown() end log.info("Finding...") @@ -78,30 +89,57 @@ function Finder:_find(prompt, process_result, process_complete) self.done = false + -- TODO: Should consider ways to allow "transformers" to be run here. + -- So that a finder can choose to "transform" the text into something much more easily usable. + local entries_processed = 0 + + local on_output = function(_, line, _) + if not line then + return + end + + if maximum_results then + entries_processed = entries_processed + 1 + if entries_processed > maximum_results then + log.info("Shutting down job early...") + self.job:shutdown() + end + end + + if vim.trim(line) ~= "" then + line = line:gsub("\n", "") + + process_result(line) + + if self.static then + table.insert(self._cached_lines, line) + end + end + end + -- TODO: How to just literally pass a list... -- TODO: How to configure what should happen here -- TODO: How to run this over and over? - self.job_id = vim.fn.jobstart(self:fn_command(prompt), { - stdout_buffered = true, + local opts = self:fn_command(prompt) + if not opts then return end - on_stdout = function(_, data, _) - for _, line in ipairs(data) do - if vim.trim(line) ~= "" then - process_result(line) + self.job = Job:new { + command = opts.command, + args = opts.args, - if self.static then - table.insert(self._cached_lines, line) - end - end - end - end, + maximum_results = self.maximum_results, + + on_stdout = on_output, + on_stderr = on_output, on_exit = function() self.done = true process_complete() end, - }) + } + + self.job:start() end --- Return a new Finder diff --git a/lua/telescope/init.lua b/lua/telescope/init.lua index 22d3f83..8fdd56f 100644 --- a/lua/telescope/init.lua +++ b/lua/telescope/init.lua @@ -11,22 +11,15 @@ local state = require('telescope.state') local builtin = require('telescope.builtin') local telescope = { - -- .new { } - finders = finders, - pickers = pickers, - previewers = previewers, - sorters = sorters, +-- -- .new { } +-- finders = finders, +-- pickers = pickers, +-- previewers = previewers, +-- sorters = sorters, - state = state, +-- state = state, - builtin = builtin, +-- builtin = builtin, } -function __TelescopeOnLeave(prompt_bufnr) - local status = state.get_status(prompt_bufnr) - local picker = status.picker - - picker:close_windows(status) -end - return telescope diff --git a/lua/telescope/mappings.lua b/lua/telescope/mappings.lua index a3704a6..1e79900 100644 --- a/lua/telescope/mappings.lua +++ b/lua/telescope/mappings.lua @@ -52,18 +52,19 @@ keymap["control-p"] = function(prompt_bufnr, _) end keymap["enter"] = function(prompt_bufnr, results_bufnr) - local row = state.get_status(prompt_bufnr).picker:get_selection() - if row == nil then - print("Could not do anything...") + local entry = state.get_status(prompt_bufnr).picker:get_selection() + + if not entry then + print("[telescope] Nothing currently selected") return else - local line = a.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] - if line == nil then + local value = entry.value + if not value then print("Could not do anything with blank line...") return end - local sections = vim.split(line, ":") + local sections = vim.split(value, ":") local filename = sections[1] local row = tonumber(sections[2]) @@ -77,6 +78,8 @@ keymap["enter"] = function(prompt_bufnr, results_bufnr) if row and col then a.nvim_win_set_cursor(0, {row, col}) end + + vim.cmd [[stopinsert]] end end diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index d0785ae..9973c8e 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -1,12 +1,15 @@ local a = vim.api local popup = require('popup') - local log = require('telescope.log') local mappings = require('telescope.mappings') local state = require('telescope.state') local utils = require('telescope.utils') +local Entry = require('telescope.entry') +local Sorter = require('telescope.sorters').Sorter +local Previewer = require('telescope.previewers').Previewer + local pickers = {} --- Picker is the main UI that shows up to interact w/ your results. @@ -14,10 +17,6 @@ local pickers = {} local Picker = {} Picker.__index = Picker - -local Sorter = require('telescope.sorters').Sorter -local Previewer = require('telescope.previewers').Previewer - assert(Sorter) assert(Previewer) @@ -53,7 +52,9 @@ function Picker._get_window_options(max_columns, max_lines, prompt_title) } local width_padding = 10 - if max_columns < 200 then + if max_columns < 150 then + preview.width = 60 + elseif max_columns < 200 then preview.width = 80 else preview.width = 120 @@ -94,7 +95,6 @@ function Picker:find(opts) assert(finder, "Finder is required to do picking") local sorter = opts.sorter - local prompt_string = opts.prompt -- Create three windows: @@ -107,6 +107,9 @@ function Picker:find(opts) local results_win, results_opts = popup.create('', popup_opts.results) local results_bufnr = a.nvim_win_get_buf(results_win) + -- TODO: Should probably always show all the line for results win, so should implement a resize for the windows + a.nvim_win_set_option(results_win, 'wrap', false) + local preview_win, preview_opts = popup.create('', popup_opts.preview) local preview_bufnr = a.nvim_win_get_buf(preview_win) @@ -124,28 +127,15 @@ function Picker:find(opts) -- First thing we want to do is set all the lines to blank. self.max_results = popup_opts.results.height - 1 - local initial_lines = {} - for _ = 1, self.max_results do table.insert(initial_lines, "") end - vim.api.nvim_buf_set_lines(results_bufnr, 0, self.max_results, false, initial_lines) + + vim.api.nvim_buf_set_lines(results_bufnr, 0, self.max_results, false, utils.repeated_table(self.max_results, "")) local on_lines = function(_, _, _, first_line, last_line) local prompt = vim.api.nvim_buf_get_lines(prompt_bufnr, first_line, last_line, false)[1] - -- Create a closure that has all the data we need - -- We pass a function called "newResult" to get_results - -- get_results calles "newResult" every time it gets a new result - -- picker then (if available) calls sorter - -- and then appropriately places new result in the buffer. - - -- TODO: We need to fix the sorting - -- TODO: We should provide a simple fuzzy matcher in Lua for people - -- TODO: We should get all the stuff on the bottom line directly, not floating around - -- TODO: We need to handle huge lists in a good way, cause currently we'll just put too much stuff in the buffer - -- TODO: Stop having things crash if we have an error. - - local line_manager = pickers.line_manager( + self.manager = pickers.entry_manager( self.max_results, - function(index, line) + vim.schedule_wrap(function(index, entry) local row = self.max_results - index + 1 -- If it's less than 0, then we don't need to show it at all. @@ -153,43 +143,48 @@ function Picker:find(opts) return end - log.trace("Setting row", row, "with value", line) - vim.api.nvim_buf_set_lines(results_bufnr, row, row + 1, false, {line}) + -- log.info("Setting row", row, "with value", entry) + vim.api.nvim_buf_set_lines(results_bufnr, row, row + 1, false, {entry.display}) end - ) + )) local process_result = function(line) - if vim.trim(line) == "" then + local entry = Entry:new(line) + + if not entry.valid then return end - log.trace("Processing result... ", line) + log.trace("Processing result... ", entry) - local sort_score = 0 - local sort_ok + local sort_ok, sort_score = nil, 0 if sorter then sort_ok, sort_score = pcall(function () - return sorter:score(prompt, line) + return sorter:score(prompt, entry) end) if not sort_ok then - log.warn("Sorting failed with:", prompt, line, sort_score) + log.warn("Sorting failed with:", prompt, entry, sort_score) return end if sort_score == -1 then - log.trace("Filtering out result: ", line) + log.trace("Filtering out result: ", entry) return end end - line_manager:add_result(sort_score, line) + self.manager:add_entry(sort_score, entry) end - local process_complete = function() - local worst_line = self.max_results - line_manager.num_results() + local process_complete = vim.schedule_wrap(function() + local worst_line = self.max_results - self.manager.num_results() + if worst_line == 0 then + return + end + local empty_lines = utils.repeated_table(worst_line, "") - -- vim.api.nvim_buf_set_lines(results_bufnr, 0, worst_line + 1, false, empty_lines) + vim.api.nvim_buf_set_lines(results_bufnr, 0, worst_line, false, empty_lines) log.debug("Worst Line after process_complete: %s", worst_line, results_bufnr) @@ -201,7 +196,7 @@ function Picker:find(opts) -- a.nvim_buf_get_lines(results_bufnr, worst_line, self.max_results, false), -- self.line_scores -- ))) - end + end) local ok, msg = pcall(function() return finder(prompt, process_result, process_complete) @@ -229,7 +224,7 @@ function Picker:find(opts) -- TODO: Use WinLeave as well? local on_buf_leave = string.format( - [[ autocmd BufLeave ++nested ++once :lua __TelescopeOnLeave(%s)]], + [[ autocmd BufLeave ++nested ++once :lua require('telescope.pickers').on_close_prompt(%s)]], prompt_bufnr) vim.cmd([[augroup PickerInsert]]) @@ -304,11 +299,15 @@ end local ns_telescope_selection = a.nvim_create_namespace('telescope_selection') function Picker:get_selection() - return self.selection or self.max_results + return self._selection +end + +function Picker:get_selection_row() + return self._selection_row or self.max_results end function Picker:move_selection(change) - self:set_selection(self:get_selection() + change) + self:set_selection(self:get_selection_row() + change) end function Picker:set_selection(row) @@ -318,6 +317,7 @@ function Picker:set_selection(row) row = 1 end + local entry = self.manager:get_entry(self.max_results - row + 1) local status = state.get_status(self.prompt_bufnr) a.nvim_buf_clear_namespace(status.results_bufnr, ns_telescope_selection, 0, -1) @@ -334,14 +334,13 @@ function Picker:set_selection(row) -- TODO: Make sure you start exactly at the bottom selected -- TODO: Get row & text in the same obj - self.selection = row + self._selection = entry + self._selection_row = row if self.previewer then self.previewer:preview( - status.preview_win, - status.preview_bufnr, - status.results_bufnr, - row + entry, + status ) end end @@ -350,10 +349,10 @@ pickers.new = function(...) return Picker:new(...) end --- TODO: We should consider adding `process_bulk` or `bulk_line_manager` for things +-- TODO: We should consider adding `process_bulk` or `bulk_entry_manager` for things -- that we always know the items and can score quickly, so as to avoid drawing so much. -pickers.line_manager = function(max_results, set_line) - log.debug("Creating line_manager...") +pickers.entry_manager = function(max_results, set_entry) + log.debug("Creating entry_manager...") -- state contains list of -- { @@ -361,19 +360,24 @@ pickers.line_manager = function(max_results, set_line) -- line = ... -- metadata ? ... -- } - local line_state = {} + local entry_state = {} - set_line = set_line or function() end + set_entry = set_entry or function() end return setmetatable({ - add_result = function(self, score, line) + 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 + score = score or 0 - for index, item in ipairs(line_state) do + for index, item in ipairs(entry_state) do if item.score > score then return self:insert(index, { score = score, - line = line, + entry = entry, }) end @@ -385,36 +389,44 @@ pickers.line_manager = function(max_results, set_line) return self:insert({ score = score, - line = line, + entry = entry, }) end, - insert = function(self, index, item) - if item == nil then - item = index - index = #line_state + 1 + insert = function(self, index, entry) + if entry == nil then + entry = index + index = #entry_state + 1 end -- To insert something, we place at the next available index (or specified index) -- and then shift all the corresponding items one place. - local next_item + local next_entry repeat - next_item = line_state[index] + next_entry = entry_state[index] - set_line(index, item.line) - line_state[index] = item + set_entry(index, entry.entry) + entry_state[index] = entry index = index + 1 - item = next_item - until not next_item + entry = next_entry + until not next_entry end, num_results = function() - return #line_state + return #entry_state + end, + + get_ordinal = function(self, index) + return self:get_entry(index).ordinal + end, + + get_entry = function(_, index) + return (entry_state[index] or {}).entry end, _get_state = function() - return line_state + return entry_state end, }, { -- insert = @@ -428,4 +440,13 @@ pickers.line_manager = function(max_results, set_line) end + +function pickers.on_close_prompt(prompt_bufnr) + local status = state.get_status(prompt_bufnr) + local picker = status.picker + + picker:close_windows(status) +end + + return pickers diff --git a/lua/telescope/previewers.lua b/lua/telescope/previewers.lua index 91f0060..8d1a1e1 100644 --- a/lua/telescope/previewers.lua +++ b/lua/telescope/previewers.lua @@ -9,12 +9,22 @@ function Previewer:new(opts) opts = opts or {} return setmetatable({ + state = nil, + _setup_func = opts.setup, preview_fn = opts.preview_fn, }, Previewer) end -function Previewer:preview(preview_win, preview_bufnr, results_bufnr, row) - return self.preview_fn(preview_win, preview_bufnr, results_bufnr, row) +function Previewer:preview(entry, status) + if not entry then + return + end + + if not self.state and self._setup_func then + self.state = self._setup_func() + end + + return self:preview_fn(entry, status) end previewers.new = function(...) @@ -22,12 +32,12 @@ previewers.new = function(...) end previewers.vim_buffer = previewers.new { - preview_fn = function(preview_win, preview_bufnr, results_bufnr, row) - local line = vim.api.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] - if line == nil then + preview_fn = function(_, entry, status) + local value = entry.value + if value == nil then return end - local file_name = vim.split(line, ":")[1] + local file_name = vim.split(value, ":")[1] log.trace("Previewing File: %s", file_name) @@ -39,23 +49,24 @@ previewers.vim_buffer = previewers.new { -- 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)) - vim.api.nvim_win_set_buf(preview_win, bufnr) - vim.api.nvim_win_set_option(preview_win, 'wrap', false) - vim.api.nvim_win_set_option(preview_win, 'winhl', 'Normal:Normal') + vim.api.nvim_win_set_buf(status.preview_win, bufnr) + vim.api.nvim_win_set_option(status.preview_win, 'wrap', false) + vim.api.nvim_win_set_option(status.preview_win, 'winhl', 'Normal:Normal') -- vim.api.nvim_win_set_option(preview_win, 'winblend', 20) - vim.api.nvim_win_set_option(preview_win, 'signcolumn', 'no') - vim.api.nvim_win_set_option(preview_win, 'foldlevel', 100) + vim.api.nvim_win_set_option(status.preview_win, 'signcolumn', 'no') + vim.api.nvim_win_set_option(status.preview_win, 'foldlevel', 100) end, } previewers.vim_buffer_or_bat = previewers.new { - preview_fn = function(preview_win, preview_bufnr, results_bufnr, row) - local line = vim.api.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] - if line == nil then + preview_fn = function(_, entry, status) + local value = entry.value + if value == nil then return end - local file_name = vim.split(line, ":")[1] + + local file_name = vim.split(value, ":")[1] log.info("Previewing File: %s", file_name) @@ -69,18 +80,115 @@ previewers.vim_buffer_or_bat = previewers.new { -- 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)) - vim.api.nvim_win_set_buf(preview_win, bufnr) - vim.api.nvim_win_set_option(preview_win, 'wrap', false) - vim.api.nvim_win_set_option(preview_win, 'winhl', 'Normal:Normal') + vim.api.nvim_win_set_buf(status.preview_win, bufnr) + vim.api.nvim_win_set_option(status.preview_win, 'wrap', false) + vim.api.nvim_win_set_option(status.preview_win, 'winhl', 'Normal:Normal') -- vim.api.nvim_win_set_option(preview_win, 'winblend', 20) - vim.api.nvim_win_set_option(preview_win, 'signcolumn', 'no') - vim.api.nvim_win_set_option(preview_win, 'foldlevel', 100) + vim.api.nvim_win_set_option(status.preview_win, 'signcolumn', 'no') + vim.api.nvim_win_set_option(status.preview_win, 'foldlevel', 100) else - vim.api.nvim_buf_set_lines(preview_bufnr, 0, -1, false, vim.fn.systemlist(string.format('bat %s', file_name))) + vim.api.nvim_buf_set_lines(status.preview_bufnr, 0, -1, false, vim.fn.systemlist(string.format('bat %s', file_name))) end end, } +previewers.cat = previewers.new { + setup = function() + local command_string = "cat %s" + if vim.fn.executable("bat") then + command_string = "bat %s --style=grid --paging=always" + end + + return { + command_string = command_string + } + end, + + preview_fn = function(self, entry, status) + local bufnr = vim.api.nvim_create_buf(false, true) + + vim.api.nvim_win_set_buf(status.preview_win, bufnr) + + -- HACK! Requires `termopen` to accept buffer argument. + vim.cmd(string.format("noautocmd call win_gotoid(%s)", status.preview_win)) + vim.fn.termopen(string.format(self.state.command_string, entry.value)) + vim.cmd(string.format("noautocmd call win_gotoid(%s)", status.prompt_win)) + + vim.api.nvim_buf_set_name(bufnr, tostring(bufnr)) + end +} + +previewers.vimgrep = previewers.new { + setup = function() + local command_string = "cat %s" + if vim.fn.executable("bat") then + command_string = "bat %s --style=grid --paging=always --highlight-line %s -r %s:%s" + end + + return { + command_string = command_string + } + end, + + preview_fn = function(self, entry, status) + local bufnr = vim.api.nvim_create_buf(false, true) + local win_id = status.preview_win + local height = vim.api.nvim_win_get_height(win_id) + + local line = entry.value + if type(line) == "table" then + line = entry.ordinal + end + + local _, _, filename, lnum, col, text = string.find(line, [[([^:]+):(%d+):(%d+):(.*)]]) + + local context = math.floor(height / 2) + local start = math.max(0, lnum - context) + local finish = lnum + context + + vim.api.nvim_win_set_buf(status.preview_win, bufnr) + + -- HACK! Requires `termopen` to accept buffer argument. + vim.cmd(string.format("noautocmd call win_gotoid(%s)", status.preview_win)) + vim.fn.termopen(string.format(self.state.command_string, filename, lnum, start, finish)) + vim.cmd(string.format("noautocmd call win_gotoid(%s)", status.prompt_win)) + + end +} + +previewers.qflist = previewers.new { + setup = function() + local command_string = "cat %s" + if vim.fn.executable("bat") then + command_string = "bat %s --style=grid --paging=always --highlight-line %s -r %s:%s" + end + + return { + command_string = command_string + } + end, + + preview_fn = function(self, entry, status) + local bufnr = vim.api.nvim_create_buf(false, true) + local win_id = status.preview_win + local height = vim.api.nvim_win_get_height(win_id) + + local filename = entry.value.filename + local lnum = entry.value.lnum + + local context = math.floor(height / 2) + local start = math.max(0, lnum - context) + local finish = lnum + context + + vim.api.nvim_win_set_buf(status.preview_win, bufnr) + + -- HACK! Requires `termopen` to accept buffer argument. + vim.cmd(string.format("noautocmd call win_gotoid(%s)", status.preview_win)) + vim.fn.termopen(string.format(self.state.command_string, filename, lnum, start, finish)) + vim.cmd(string.format("noautocmd call win_gotoid(%s)", status.prompt_win)) + end +} + previewers.Previewer = Previewer return previewers diff --git a/lua/telescope/sorters.lua b/lua/telescope/sorters.lua index d09b53b..3754a71 100644 --- a/lua/telescope/sorters.lua +++ b/lua/telescope/sorters.lua @@ -1,5 +1,3 @@ -local ceil = math.ceil - local log = require('telescope.log') local util = require('telescope.utils') @@ -26,8 +24,9 @@ function Sorter:new(opts) }, Sorter) end -function Sorter:score(prompt, line) - return self:scoring_function(prompt, line) +function Sorter:score(prompt, entry) + -- TODO: Decide if we actually want to check the type every time. + return self:scoring_function(prompt, type(entry) == "string" and entry or entry.ordinal) end function sorters.new(...) @@ -143,6 +142,10 @@ sorters.get_norcalli_sorter = function() return -1 end + if #prompt > 2 and denominator < 0.5 then + return -1 + end + return 1 / denominator end } diff --git a/lua/telescope/state.lua b/lua/telescope/state.lua index 0ae31dd..8399c05 100644 --- a/lua/telescope/state.lua +++ b/lua/telescope/state.lua @@ -1,14 +1,14 @@ local state = {} -state._statuses = {} +TelescopeGlobalState = TelescopeGlobalState or {} --- Set the status for a particular prompt bufnr function state.set_status(prompt_bufnr, status) - state._statuses[prompt_bufnr] = status + TelescopeGlobalState[prompt_bufnr] = status end function state.get_status(prompt_bufnr) - return state._statuses[prompt_bufnr] or {} + return TelescopeGlobalState[prompt_bufnr] or {} end function state.clear_status(prompt_bufnr) diff --git a/lua/tests/telescope_spec.lua b/lua/tests/telescope_spec.lua index 9960772..de2de2e 100644 --- a/lua/tests/telescope_spec.lua +++ b/lua/tests/telescope_spec.lua @@ -1,6 +1,7 @@ require('plenary.test_harness'):setup_busted() local log = require('telescope.log') +log.level = 'info' -- log.use_console = false local pickers = require('telescope.pickers') @@ -19,50 +20,50 @@ describe('Picker', function() describe('process_result', function() it('works with one entry', function() - local lines_manager = pickers.line_manager(5, nil) + local manager = pickers.entry_manager(5, nil) - lines_manager:add_result(1, "hello") + manager:add_entry(1, "hello") - assert.are.same(1, lines_manager:_get_state()[1].score) + assert.are.same(1, manager:_get_state()[1].score) end) it('works with two entries', function() - local lines_manager = pickers.line_manager(5, nil) + local manager = pickers.entry_manager(5, nil) - lines_manager:add_result(1, "hello") - lines_manager:add_result(2, "later") + manager:add_entry(1, "hello") + manager:add_entry(2, "later") - assert.are.same("hello", lines_manager:_get_state()[1].line) - assert.are.same("later", lines_manager:_get_state()[2].line) + assert.are.same("hello", manager:get_ordinal(1)) + assert.are.same("later", manager:get_ordinal(2)) end) it('calls functions when inserting', function() local called_count = 0 - local lines_manager = pickers.line_manager(5, function() called_count = called_count + 1 end) + local manager = pickers.entry_manager(5, function() called_count = called_count + 1 end) assert(called_count == 0) - lines_manager:add_result(1, "hello") + manager:add_entry(1, "hello") assert(called_count == 1) end) it('calls functions when inserting twice', function() local called_count = 0 - local lines_manager = pickers.line_manager(5, function() called_count = called_count + 1 end) + local manager = pickers.entry_manager(5, function() called_count = called_count + 1 end) assert(called_count == 0) - lines_manager:add_result(1, "hello") - lines_manager:add_result(2, "world") + manager:add_entry(1, "hello") + manager:add_entry(2, "world") assert(called_count == 2) end) it('correctly sorts lower scores', function() local called_count = 0 - local lines_manager = pickers.line_manager(5, function() called_count = called_count + 1 end) - lines_manager:add_result(5, "worse result") - lines_manager:add_result(2, "better result") + local manager = pickers.entry_manager(5, function() called_count = called_count + 1 end) + manager:add_entry(5, "worse result") + manager:add_entry(2, "better result") - assert.are.same("better result", lines_manager:_get_state()[1].line) - assert.are.same("worse result", lines_manager:_get_state()[2].line) + assert.are.same("better result", manager:get_ordinal(1)) + assert.are.same("worse result", manager:get_ordinal(2)) -- once to insert "worse" -- once to insert "better" @@ -72,11 +73,11 @@ describe('Picker', function() it('respects max results', function() local called_count = 0 - local lines_manager = pickers.line_manager(1, function() called_count = called_count + 1 end) - lines_manager:add_result(2, "better result") - lines_manager:add_result(5, "worse result") + local manager = pickers.entry_manager(1, function() called_count = called_count + 1 end) + manager:add_entry(2, "better result") + manager:add_entry(5, "worse result") - assert.are.same("better result", lines_manager:_get_state()[1].line) + assert.are.same("better result", manager:get_ordinal(1)) -- once to insert "worse" -- once to insert "better" @@ -86,10 +87,36 @@ describe('Picker', function() -- TODO: We should decide if we want to add this or not. -- it('should handle no scores', function() - -- local lines_manager = pickers.line_manager(5, nil) + -- local manager = pickers.entry_manager(5, nil) - -- lines_manager:add_result(nil, + -- manager:add_entry(nil, -- end) + + it('should allow simple entries', function() + local manager = pickers.entry_manager(5) + + local counts_executed = 0 + manager:add_entry(1, setmetatable({}, { + __index = function(t, k) + local val = nil + if k == "ordinal" then + counts_executed = counts_executed + 1 + + -- This could be expensive, only call later + val = "wow" + end + + rawset(t, k, val) + return val + end, + })) + + assert.are.same("wow", manager:get_ordinal(1)) + assert.are.same("wow", manager:get_ordinal(1)) + assert.are.same("wow", manager:get_ordinal(1)) + + assert.are.same(1, counts_executed) + end) end) describe('ngrams', function()