diff --git a/lua/telescope/algos/string_distance.lua b/lua/telescope/algos/string_distance.lua new file mode 100644 index 0000000..de40e14 --- /dev/null +++ b/lua/telescope/algos/string_distance.lua @@ -0,0 +1,49 @@ + +local function min(a, b, c) + local min_val = a + + if b < min_val then min_val = b end + if c < min_val then min_val = c end + + return min_val + +end + +---------------------------------- +--- Levenshtein distance function. +-- @tparam string s1 +-- @tparam string s2 +-- @treturn number the levenshtein distance +-- @within Metrics +return function(s1, s2) + if s1 == s2 then return 0 end + if s1:len() == 0 then return s2:len() end + if s2:len() == 0 then return s1:len() end + if s1:len() < s2:len() then s1, s2 = s2, s1 end + + local t = {} + for i=1, #s1+1 do + t[i] = {i-1} + end + + for i=1, #s2+1 do + t[1][i] = i-1 + end + + local cost + for i=2, #s1+1 do + + for j=2, #s2+1 do + cost = (s1:sub(i-1,i-1) == s2:sub(j-1,j-1) and 0) or 1 + t[i][j] = min( + t[i-1][j] + 1, + t[i][j-1] + 1, + t[i-1][j-1] + cost) + end + + end + + return t[#s1+1][#s2+1] + +end + diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index 1601d09..0b7e185 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -2,8 +2,11 @@ local a = vim.api local finders = {} +---@class Finder local Finder = {} + Finder.__index = Finder +Finder.__call = function(t, ... ) return t:_find(...) end --- Create a new finder command --- @@ -26,52 +29,37 @@ function Finder:new(opts) return setmetatable({ fn_command = opts.fn_command, responsive = opts.responsive, + state = {}, job_id = -1, }, Finder) end -function Finder:get_results(win, bufnr, prompt) - if self.job_id > 0 then - -- Make sure we kill old jobs. +-- 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) + if (self.state.job_id or 0) > 0 then vim.fn.jobstop(self.job_id) end - self.job_id = vim.fn.jobstart(self.fn_command(prompt), { - -- TODO: Decide if we want this or don't want this. + -- 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, on_stdout = function(_, data, _) - a.nvim_buf_set_lines(bufnr, -1, -1, false, data) - end, - - on_exit = function() - -- TODO: Add possibility to easily highlight prompt within buffer - -- without having to do weird stuff and with it actually working... - if false then - vim.fn.matchadd("Type", "\\<" .. prompt .. "\\>", 1, -1, {window = win}) + for _, line in ipairs(data) do + process_result(line) end - end, + end }) - - --[[ - local function get_rg_results(bufnr, search_string) - local start_time = vim.fn.reltime() - - vim.fn.jobstart(string.format('rg %s', search_string), { - cwd = '/home/tj/build/neovim', - - on_stdout = function(job_id, data, event) - vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, data) - end, - - on_exit = function() - print("Finished in: ", vim.fn.reltimestr(vim.fn.reltime(start_time))) - end, - - stdout_buffer = true, - }) - end - --]] end --- Return a new Finder @@ -81,4 +69,6 @@ finders.new = function(...) return Finder:new(...) end +finders.Finder = Finder + return finders diff --git a/lua/telescope/init.lua b/lua/telescope/init.lua index 76abb5a..75f6d38 100644 --- a/lua/telescope/init.lua +++ b/lua/telescope/init.lua @@ -6,12 +6,16 @@ local finders = require('telescope.finders') local pickers = require('telescope.pickers') local previewers = require('telescope.previewers') +local sorters = require('telescope.sorters') local state = require('telescope.state') local telescope = { + -- .new { } finders = finders, pickers = pickers, previewers = previewers, + sorters = sorters, + state = state, } @@ -22,16 +26,4 @@ function __TelescopeOnLeave(prompt_bufnr) picker:close_windows(status) end --- TODO: Probably could attach this with nvim_buf_attach, and then I don't have to do the ugly global function stuff -function __TelescopeOnChange(prompt_bufnr, prompt, results_bufnr, results_win) - local line = vim.api.nvim_buf_get_lines(prompt_bufnr, 0, -1, false)[1] - local prompt_input = string.sub(line, #prompt + 1) - - local status = state.get_status(prompt_bufnr) - local finder = status.finder - - vim.api.nvim_buf_set_lines(results_bufnr, 0, -1, false, {}) - local results = finder:get_results(results_win, results_bufnr, prompt_input) -end - return telescope diff --git a/lua/telescope/mappings.lua b/lua/telescope/mappings.lua index b779b2f..3c6de18 100644 --- a/lua/telescope/mappings.lua +++ b/lua/telescope/mappings.lua @@ -49,7 +49,7 @@ local function update_current_selection(prompt_bufnr, results_bufnr, row) if status.previewer then vim.g.got_here = true - status.previewer.fn( + status.previewer:preview( status.preview_win, status.preview_bufnr, status.results_bufnr, diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index a8624b0..f9f64df 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -3,6 +3,7 @@ local popup = require('popup') local mappings = require('telescope.mappings') local state = require('telescope.state') +local utils = require('telescope.utils') local pickers = {} @@ -19,8 +20,15 @@ function Picker:new(opts) end -function Picker:find(finder) - local prompt_string = 'Find File' +function Picker:find(opts) + opts = opts or {} + + local finder = opts.finder + assert(finder, "Finder is required to do picking") + + local sorter = opts.sorter + + local prompt_string = opts.prompt -- Create three windows: -- 1. Prompt window -- 2. Options window @@ -75,26 +83,71 @@ function Picker:find(finder) -- a.nvim_buf_set_option(prompt_bufnr, 'buftype', 'prompt') -- vim.fn.prompt_setprompt(prompt_bufnr, prompt_string) - vim.api.nvim_buf_attach(prompt_bufnr, true, { - on_lines = vim.schedule_wrap(function(_, _, _, first_line, last_line) - local line = vim.api.nvim_buf_get_lines(prompt_bufnr, first_line, last_line, false)[1] + local on_lines = function(_, _, _, first_line, last_line) + local prompt = vim.api.nvim_buf_get_lines(prompt_bufnr, first_line, last_line, false)[1] - vim.api.nvim_buf_set_lines(results_bufnr, 0, -1, false, {}) - local results = finder:get_results(results_win, results_bufnr, line) - end), + vim.api.nvim_buf_set_lines(results_bufnr, 0, -1, false, {}) + + -- 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. + + local line_scores = {} + + -- 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. + finder(prompt, function(line) + if sorter then + local sort_score = sorter:score(prompt, line) + if sort_score == -1 then + return + end + + -- { 7, 3, 1, 1 } + -- 2 + for row, row_score in utils.reversed_ipairs(line_scores) do + if row_score > sort_score then + -- Insert line at row + vim.api.nvim_buf_set_lines(results_bufnr, row, row, false, { + string.format("%s // %s %s", line, sort_score, row) + }) + + -- Insert current score in the table + table.insert(line_scores, row + 1, sort_score) + + -- All done :) + return + end + end + + -- Worst score so far, so add to end + vim.api.nvim_buf_set_lines(results_bufnr, -1, -1, false, {line}) + table.insert(line_scores, sort_score) + else + -- Just always append to the end of the buffer if this is all you got. + vim.api.nvim_buf_set_lines(results_bufnr, -1, -1, false, {line}) + end + end) + -- local results = finder:get_results(results_win, results_bufnr, line) + end + + -- Call this once to pre-populate if it makes sense + vim.schedule_wrap(on_lines(nil, nil, nil, 0, 1)) + + -- Register attach + vim.api.nvim_buf_attach(prompt_bufnr, true, { + on_lines = vim.schedule_wrap(on_lines), on_detach = function(...) -- print("DETACH:", ...) end, }) - -- -- TODO: Please use the cool autocmds once you get off your lazy bottom and finish the PR ;) - -- local autocmd_string = string.format( - -- [[ autocmd TextChanged,TextChangedI :lua __TelescopeOnChange(%s, "%s", %s, %s)]], - -- prompt_bufnr, - -- '', - -- results_bufnr, - -- results_win) -- TODO: Use WinLeave as well? local on_buf_leave = string.format( diff --git a/lua/telescope/previewers.lua b/lua/telescope/previewers.lua index a36ec5a..ccbd7db 100644 --- a/lua/telescope/previewers.lua +++ b/lua/telescope/previewers.lua @@ -3,14 +3,45 @@ local previewers = {} local Previewer = {} Previewer.__index = Previewer -function Previewer:new(fn) +function Previewer:new(opts) + opts = opts or {} + return setmetatable({ - fn = fn, + 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) +end + previewers.new = function(...) return Previewer:new(...) end +previewers.vim_buffer = previewers.new { + preview_fn = function(preview_win, preview_bufnr, results_bufnr, row) + assert(preview_bufnr) + + local line = vim.api.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] + local file_name = vim.split(line, ":")[1] + + -- print(file_name) + -- 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) + + -- 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_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) + end, +} + return previewers diff --git a/lua/telescope/sorters.lua b/lua/telescope/sorters.lua new file mode 100644 index 0000000..d5fb6f9 --- /dev/null +++ b/lua/telescope/sorters.lua @@ -0,0 +1,32 @@ +local sorters = {} + + +local Sorter = {} +Sorter.__index = Sorter + +---@class Sorter +--- Sorter sorts a list of results by return a single integer for a line, +--- given a prompt +--- +--- Lower number is better (because it's like a closer match) +--- But, any number below 0 means you want that line filtered out. +--- @param scoring_function function Function that has the interface: +-- (sorter, prompt, line): number +function Sorter:new(opts) + opts = opts or {} + + return setmetatable({ + state = {}, + scoring_function = opts.scoring_function, + }, Sorter) +end + +function Sorter:score(prompt, line) + return self:scoring_function(prompt, line) +end + +function sorters.new(...) + return Sorter:new(...) +end + +return sorters diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua new file mode 100644 index 0000000..196453a --- /dev/null +++ b/lua/telescope/utils.lua @@ -0,0 +1,14 @@ +local utils = {} + +local function reversedipairsiter(t, i) + i = i - 1 + if i ~= 0 then + return i, t[i] + end +end + +utils.reversed_ipairs = function(t) + return reversedipairsiter, t, #t + 1 +end + +return utils diff --git a/scratch/file_finder.lua b/scratch/file_finder.lua new file mode 100644 index 0000000..8b5bb7b --- /dev/null +++ b/scratch/file_finder.lua @@ -0,0 +1,84 @@ +local telescope = require('telescope') + +-- Goals: +-- 1. You pick a directory +-- 2. We `git ls-files` in that directory ONCE and ONLY ONCE to get the results. +-- 3. You can fuzzy find those results w/ fzf +-- 4. Select one and go to file. + + +--[[ +ls_files_job.start() +fzf_job.stdin = ls_files_job.stdout + + self.stdin = vim.loop.new_pipe(false) + -> self.stdin = finder.stdout + + + -- Finder: + intermediary_pipe = self.stdout + + -- repeat send this pipe when we want to get new filtering + self.stdin = intermediary_pipe + + + -- Filter + Sort + ok, we could do scoring + cutoff and have that always be the case. + + OR + + filter takes a function, signature (prompt: str, line: str): number + + => echo $line | fzf --filter "prompt" + return stdout != "" + + => lua_fuzzy_finder(prompt, line) return true if good enough + + TODO: Rename everything to be more clear like the name below. + IFilterSorterAbstractFactoryGeneratorv1ProtoBeta + +--]] + +local string_distance = require('telescope.algos.string_distance') + +local file_finder = telescope.finders.new { + fn_command = function(self, prompt) + -- todo figure out how to cache this later + if false then + if self[prompt] == nil then + self[prompt] = nil + end + + return self[prompt] + else + return 'git ls-files' + end + end, +} + +local file_sorter = telescope.sorters.new { + scoring_function = function(self, prompt, line) + if prompt == '' then return 0 end + if not line then return -1 end + + local dist = string_distance(prompt, line) + -- if dist > (0.75 * #line) and #prompt > 3 then + -- return -1 + -- end + + return dist + end +} + +local file_previewer = telescope.previewers.vim_buffer + +local file_picker = telescope.pickers.new { + previewer = file_previewer +} + +file_picker:find { + prompt = 'Find File', + finder = file_finder, + sorter = file_sorter, +} + diff --git a/scratch/simple_rg.lua b/scratch/simple_rg.lua index c6a8481..5102611 100644 --- a/scratch/simple_rg.lua +++ b/scratch/simple_rg.lua @@ -5,37 +5,19 @@ local telescope = require('telescope') -- When updating the table, we should call filter on those items -- and then only display ones that pass the filter local rg_finder = telescope.finders.new { - fn_command = function(prompt) + fn_command = function(self, prompt) return string.format('rg --vimgrep %s', prompt) end, responsive = false } - local p = telescope.pickers.new { - previewer = telescope.previewers.new(function(preview_win, preview_bufnr, results_bufnr, row) - assert(preview_bufnr) - - local line = vim.api.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] - local file_name = vim.split(line, ":")[1] - - -- print(file_name) - -- 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) - - -- 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_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) - end) + previewer = telescope.previewers.vim_buffer +} +p:find { + prompt = 'grep', + finder = rg_finder } -p:find(rg_finder) + diff --git a/scratch/slow_proc.sh b/scratch/slow_proc.sh index 6326991..bbcf8bf 100755 --- a/scratch/slow_proc.sh +++ b/scratch/slow_proc.sh @@ -4,3 +4,7 @@ sleep 1 echo "cool" sleep 1 echo "world" +sleep 1 +echo "x" +sleep 1 +echo "y" diff --git a/scratch/string_distance_stuff.lua b/scratch/string_distance_stuff.lua new file mode 100644 index 0000000..283f00f --- /dev/null +++ b/scratch/string_distance_stuff.lua @@ -0,0 +1,7 @@ + +local string_distance = require('telescope.algos.string_distance') + +print(string_distance("hello", "help")) +print(string_distance("hello", "hello")) +print(string_distance("hello", "asdf")) +