diff --git a/lua/telescope/_private/NGRam.lua b/lua/telescope/_private/NGRam.lua new file mode 100644 index 0000000..15006c6 --- /dev/null +++ b/lua/telescope/_private/NGRam.lua @@ -0,0 +1,109 @@ +local NGram = {} +NGram.__index = NGram + +function NGram:new(opts) + -- TODO: Add padding + opts = opts or {} + return setmetatable({ + N = opts.N or 2, + split = opts.split or "/", + _depth = 5, + _grams = setmetatable({}, utils.default_table_mt) + }, self) +end + +local min = math.min + +function NGram:_split(word) + local word_len = #word + + local result = {} + for i = 1, word_len - 1 do + -- for j = i + (self.N - 1), min(i + self._depth - 1, word_len) do + -- table.insert(result, string.sub(word, i, j)) + -- end + table.insert(result, string.sub(word, i, i + self.N - 1)) + end + + return result +end + +-- local function pairsByKeys (t, f) +-- local a = {} +-- for n in pairs(t) do table.insert(a, n) end +-- table.sort(a, f) +-- local i = 0 -- iterator variable +-- local iter = function () -- iterator function +-- i = i + 1 +-- if a[i] == nil then return nil +-- else return a[i], t[a[i]] +-- end +-- end +-- return iter +-- end + +function NGram:add(word) + local split_word = self:_split(word) + + for _, k in ipairs(split_word) do + local counts = self._grams[k] + if counts[word] == nil then + counts[word] = 0 + end + + counts[word] = counts[word] + 1 + end +end + +function NGram:_items_sharing_ngrams(query) + local split_query = self:_split(query) + + -- Matched string to number of N-grams shared with the query string. + local shared = {} + + local remaining = {} + + for _, ngram in ipairs(split_query) do + remaining = {} + for match, count in pairs(self._grams[ngram] or {}) do + remaining[match] = remaining[match] or count + + if remaining[match] > 0 then + remaining[match] = remaining[match] - 1 + shared[match] = (shared[match] or 0) + 1 + end + end + end + + return shared +end + +function NGram:search(query, show_values) + local sharing_ngrams = self:_items_sharing_ngrams(query) + + local results = {} + for name, count in pairs(sharing_ngrams) do + local allgrams = #query + #name - (2 * self.N) - count + 2 + table.insert(results, {name, count / allgrams}) + end + + table.sort(results, function(left, right) + return left[2] > right[2] + end) + + if not show_values then + for k, v in ipairs(results) do + results[k] = v[1] + end + end + + return results +end + +function NGram:find(query) + return self:search(query)[1] +end + +function NGram:score(query) + return (self:search(query, true)[1] or {})[2] or 0 +end diff --git a/lua/telescope/builtin.lua b/lua/telescope/builtin.lua index cd7e4ab..43e57fc 100644 --- a/lua/telescope/builtin.lua +++ b/lua/telescope/builtin.lua @@ -8,50 +8,20 @@ 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') local builtin = {} -local ifnil = function(x, was_nil, was_not_nil) if x == nil then return was_nil else return was_not_nil end end - builtin.git_files = function(opts) - opts = opts or {} - - local show_preview = ifnil(opts.show_preview, true, opts.show_preview) - - local file_finder = finders.new { - static = true, - - fn_command = function(self) - return { - command = 'git', - args = {'ls-files'} - } - end, - } - - local file_previewer = previewers.cat - - local file_picker = pickers.new { - previewer = show_preview and file_previewer, - - selection_strategy = opts.selection_strategy, - } - - -- local file_sorter = telescope.sorters.get_ngram_sorter() - -- local file_sorter = require('telescope.sorters').get_levenshtein_sorter() - local file_sorter = sorters.get_norcalli_sorter() - - file_picker:find { - prompt = 'Simple File', - finder = file_finder, - sorter = file_sorter, - - border = opts.border, - borderchars = opts.borderchars, - } + pickers.new(opts, { + prompt = 'Git File', + finder = finders.new_oneshot_job({ "git", "ls-files" }), + previewer = previewers.cat, + sorter = sorters.get_norcalli_sorter(), + }):find() end -builtin.live_grep = function() +builtin.live_grep = function(opts) local live_grepper = finders.new { maximum_results = 1000, @@ -68,15 +38,13 @@ builtin.live_grep = function() 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() + 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 @@ -102,15 +70,9 @@ builtin.live_grep = function() -- end -- end -- } - - file_picker:find { - prompt = 'Live Grep', - finder = live_grepper, - sorter = oldfiles_sorter, - } end -builtin.lsp_references = function() +builtin.lsp_references = function(opts) local params = vim.lsp.util.make_position_params() params.context = { includeDeclaration = true } @@ -120,87 +82,34 @@ builtin.lsp_references = function() 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 + local results = utils.quickfix_items_to_entries(locations) 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(), - } + local reference_picker = pickers.new(opts, { + prompt = 'LSP References', + finder = finders.new_table(results), + previewer = previewers.qflist, + sorter = sorters.get_norcalli_sorter(), + }):find() end -builtin.quickfix = function() +builtin.quickfix = function(opts) local locations = vim.fn.getqflist() - - local results = {} - for _, entry in ipairs(locations) do - if not entry.filename then - entry.filename = vim.api.nvim_buf_get_name(entry.bufnr) - end - - 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 + local results = utils.quickfix_items_to_entries(locations) if vim.tbl_isempty(results) then return end - local quickfix_finder = finders.new { - results = results - } - - local quickfix_previewer = previewers.qflist - local quickfix_picker = pickers.new { - previewer = quickfix_previewer - } - - quickfix_picker:find { - prompt = 'Quickfix', - finder = quickfix_finder, - sorter = sorters.get_norcalli_sorter(), - } + pickers.new(opts, { + prompt = 'Quickfix', + finder = finders.new_table(results), + previewer = previewers.qflist, + sorter = sorters.get_norcalli_sorter(), + }):find() end builtin.grep_string = function(opts) @@ -208,42 +117,23 @@ builtin.grep_string = function(opts) local search = opts.search or vim.fn.expand("") - local grepper = finders.new { - maximum_results = 10000, - - -- TODO: We can optimize these. - -- static = true, - - fn_command = function() - return { - command = 'rg', - args = {"--vimgrep", search}, - } - end - } - - local file_picker = pickers.new { - previewer = previewers.vimgrep - } - - file_picker:find { - prompt = 'Live Grep', - finder = grepper, + local file_picker = pickers.new(opts, { + prompt = 'Find Word', + finder = finders.new_oneshot_job {'rg', '--vimgrep', search}, + previewer = previewers.vimgrep, sorter = sorters.get_norcalli_sorter(), - } + }):find() end -builtin.oldfiles = function() - local oldfiles_finder = finders.new { - results = vim.v.oldfiles - } - local file_picker = pickers.new{} - - file_picker:find { +builtin.oldfiles = function(opts) + pickers.new(opts, { prompt = 'Oldfiles', - finder = oldfiles_finder, - sorter = sorters.get_norcalli_sorter() - } + finder = finders.new_table(vim.tbl_filter(function(val) + return 0 ~= vim.fn.filereadable(val) + end, vim.v.oldfiles)), + sorter = sorters.get_norcalli_sorter(), + previewer = previewers.cat, + }):find() end return builtin diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index 7866c87..ae32370 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -1,17 +1,19 @@ local Job = require('plenary.job') local log = require('telescope.log') +local utils = require('telescope.utils') local finders = {} - -- TODO: We should make a few different "FinderGenerators": -- SimpleListFinder(my_list) -- FunctionFinder(my_func) -- JobFinder(my_job_args) ---@class Finder -local Finder = {} +local Finder = { + hello = "world" +} Finder.__index = Finder Finder.__call = function(t, ... ) return t:_find(...) end @@ -35,7 +37,7 @@ function Finder:new(opts) -- string -- list -- ... - return setmetatable({ + local obj = setmetatable({ results = opts.results, fn_command = opts.fn_command, @@ -45,7 +47,9 @@ function Finder:new(opts) -- Maximum number of results to process. -- Particularly useful for live updating large queries. maximum_results = opts.maximum_results, - }, Finder) + }, self) + + return obj end -- Probably should use the word apply here, since we're apply the callback passed to us by @@ -145,8 +149,38 @@ end --- Return a new Finder -- --@return Finder -finders.new = function(...) - return Finder:new(...) +finders.new = function(opts) + return Finder:new(opts) +end + +-- TODO: Is this worth making? +-- finders.new_responsive_job = function(opts) +-- return finders.new { +-- maximum_results = get_default(opts.maximum_results, 2000), +-- } +-- end + +finders.new_oneshot_job = function(command_list) + command_list = vim.deepcopy(command_list) + + local command = table.remove(command_list, 1) + + return finders.new { + static = true, + + fn_command = function() + return { + command = command, + args = command_list, + } + end, + } +end + +finders.new_table = function(t) + return finders.new { + results = t + } end -- We should add a few utility functions here... @@ -155,6 +189,6 @@ end -- finders.new_one_shot_job -- finders.new_table -finders.Finder = Finder +-- finders.Finder = Finder return finders diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index ce01f23..e75f43e 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -1,5 +1,6 @@ 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,15 +9,23 @@ 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 has_devicons, devicons = pcall(require, 'nvim-web-devicons') +local get_default = utils.get_default + +-- TODO: Make this work with deep extend I think. +local extend = function(opts, defaults) + local result = vim.deepcopy(opts or {}) + for k, v in pairs(defaults or {}) do + if result[k] == nil then + result[k] = v + end + end + + return result +end local pickers = {} -local ifnil = function(x, was_nil, was_not_nil) if x == nil then return was_nil else return was_not_nil end end - local default_mappings = { i = { [""] = actions.move_selection_next, @@ -42,58 +51,60 @@ local default_mappings = { local Picker = {} Picker.__index = Picker -assert(Sorter) -assert(Previewer) - ----@class PickOpts ----@field filter Sorter ----@field maps table ----@field unseen string - --- Create new picker ---- @param opts PickOpts function Picker:new(opts) opts = opts or {} return setmetatable({ - filter = opts.filter, + prompt = opts.prompt, + + finder = opts.finder, + sorter = opts.sorter, previewer = opts.previewer, - maps = opts.maps, + + mappings = get_default(opts.mappings, default_mappings), get_window_options = opts.get_window_options, - selection_strategy = opts.selection_strategy, + + window = { + border = get_default(opts.border, {}), + borderchars = get_default(opts.borderchars, { '─', '│', '─', '│', '┌', '┐', '┘', '└'}), + }, + + preview_cutoff = get_default(opts.preview_cutoff, 120), }, Picker) end -function Picker:get_window_options(max_columns, max_lines, prompt_title, find_options) - - local popup_border = ifnil(find_options.border, {}, find_options.border) +function Picker:get_window_options(max_columns, max_lines, prompt_title) + local popup_border = self.window.border + local popup_borderchars = self.window.borderchars local preview = { border = popup_border, - borderchars = find_options.borderchars or nil, + borderchars = popup_borderchars, enter = false, highlight = false } local results = { border = popup_border, - borderchars = find_options.borderchars or nil, + borderchars = popup_borderchars, enter = false, } local prompt = { title = prompt_title, border = popup_border, - borderchars = find_options.borderchars or nil, + borderchars = popup_borderchars, enter = true } -- TODO: Test with 120 width terminal local width_padding = 10 - if not self.previewer or max_columns < find_options.preview_cutoff then + if not self.previewer or max_columns < self.preview_cutoff then + width_padding = 2 preview.width = 0 elseif max_columns < 150 then width_padding = 5 @@ -110,7 +121,7 @@ function Picker:get_window_options(max_columns, max_lines, prompt_title, find_op local base_height if max_lines < 40 then - base_height = math.floor(max_lines * 0.5) + base_height = math.min(math.floor(max_lines * 0.8), max_lines - 8) else base_height = math.floor(max_lines * 0.8) end @@ -143,21 +154,10 @@ function Picker:get_window_options(max_columns, max_lines, prompt_title, find_op } end --- opts.preview_cutoff = 120 -function Picker:find(opts) - opts = opts or {} - - if opts.preview_cutoff == nil then - opts.preview_cutoff = 120 - end - - opts.borderchars = opts.borderchars or { '─', '│', '─', '│', '┌', '┐', '┘', '└'} - - local finder = opts.finder - assert(finder, "Finder is required to do picking") - - local sorter = opts.sorter - local prompt_string = opts.prompt +function Picker:find() + local prompt_string = assert(self.prompt, "Prompt is required.") + local finder = assert(self.finder, "Finder is required to do picking") + local sorter = self.sorter self.original_win_id = a.nvim_get_current_win() @@ -165,7 +165,7 @@ function Picker:find(opts) -- 1. Prompt window -- 2. Options window -- 3. Preview window - local popup_opts = self:get_window_options(vim.o.columns, vim.o.lines, prompt_string, opts) + local popup_opts = self:get_window_options(vim.o.columns, vim.o.lines, prompt_string) -- TODO: Add back the borders after fixing some stuff in popup.nvim local results_win, results_opts = popup.create('', popup_opts.results) @@ -338,8 +338,7 @@ function Picker:find(opts) finder = finder, }) - -- mappings.set_keymap(prompt_bufnr, results_bufnr) - mappings.apply_keymap(prompt_bufnr, opts.mappings or default_mappings) + mappings.apply_keymap(prompt_bufnr, self.mappings) vim.cmd [[startinsert]] end @@ -462,8 +461,9 @@ function Picker:set_selection(row) end end -pickers.new = function(...) - return Picker:new(...) +pickers.new = function(opts, defaults) + opts = extend(opts, defaults) + return Picker:new(opts) end -- TODO: We should consider adding `process_bulk` or `bulk_entry_manager` for things diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index 7ddc428..eba24a6 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -1,5 +1,25 @@ local utils = {} +utils.if_nil = function(x, was_nil, was_not_nil) + if x == nil then + return was_nil + else + return was_not_nil + end +end + +utils.get_default = function(x, default) + return utils.if_nil(x, default, x) +end + +utils.get_lazy_default = function(x, defaulter, ...) + if x == nil then + return defaulter(...) + else + return x + end +end + local function reversedipairsiter(t, i) i = i - 1 if i ~= 0 then @@ -27,119 +47,31 @@ utils.repeated_table = function(n, val) return empty_lines end - -local NGram = {} -NGram.__index = NGram - -function NGram:new(opts) - -- TODO: Add padding - opts = opts or {} - return setmetatable({ - N = opts.N or 2, - split = opts.split or "/", - _depth = 5, - _grams = setmetatable({}, utils.default_table_mt) - }, self) -end - -local min = math.min - -function NGram:_split(word) - local word_len = #word - - local result = {} - for i = 1, word_len - 1 do - -- for j = i + (self.N - 1), min(i + self._depth - 1, word_len) do - -- table.insert(result, string.sub(word, i, j)) - -- end - table.insert(result, string.sub(word, i, i + self.N - 1)) - end - - return result -end - --- local function pairsByKeys (t, f) --- local a = {} --- for n in pairs(t) do table.insert(a, n) end --- table.sort(a, f) --- local i = 0 -- iterator variable --- local iter = function () -- iterator function --- i = i + 1 --- if a[i] == nil then return nil --- else return a[i], t[a[i]] --- end --- end --- return iter --- end - -function NGram:add(word) - local split_word = self:_split(word) - - for _, k in ipairs(split_word) do - local counts = self._grams[k] - if counts[word] == nil then - counts[word] = 0 - end - - counts[word] = counts[word] + 1 - end -end - -function NGram:_items_sharing_ngrams(query) - local split_query = self:_split(query) - - -- Matched string to number of N-grams shared with the query string. - local shared = {} - - local remaining = {} - - for _, ngram in ipairs(split_query) do - remaining = {} - for match, count in pairs(self._grams[ngram] or {}) do - remaining[match] = remaining[match] or count - - if remaining[match] > 0 then - remaining[match] = remaining[match] - 1 - shared[match] = (shared[match] or 0) + 1 - end - end - end - - return shared -end - -function NGram:search(query, show_values) - local sharing_ngrams = self:_items_sharing_ngrams(query) - +utils.quickfix_items_to_entries = function(locations) local results = {} - for name, count in pairs(sharing_ngrams) do - local allgrams = #query + #name - (2 * self.N) - count + 2 - table.insert(results, {name, count / allgrams}) - end - table.sort(results, function(left, right) - return left[2] > right[2] - end) + 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 + ) - if not show_values then - for k, v in ipairs(results) do - results[k] = v[1] - end + table.insert(results, { + valid = true, + value = entry, + ordinal = vimgrep_str, + display = vimgrep_str, + }) end return results end -function NGram:find(query) - return self:search(query)[1] -end - -function NGram:score(query) - return (self:search(query, true)[1] or {})[2] or 0 -end - utils.new_ngram = function() - return NGram:new() + return require("telescope._private.NGram"):new() end return utils diff --git a/scratch/rocker_example.lua b/scratch/rocker_example.lua new file mode 100644 index 0000000..0e73114 --- /dev/null +++ b/scratch/rocker_example.lua @@ -0,0 +1,24 @@ + + +builtin.git_files = function(opts) + opts = opts or {} + + opts.show_preview = get_default(opts.show_preview, true) + + opts.finder = opts.finder or finders.new { + static = true, + + fn_command = function() + return { + command = 'git', + args = {'ls-files'} + } + end, + } + + opts.prompt = opts.prompt or 'Simple File' + opts.previewer = opts.previewer or previewers.cat + opts.sorter = opts.sorter or sorters.get_norcalli_sorter() + + pickers.new(opts):find() +end