diff --git a/README.md b/README.md index 03fabc7..f318b39 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ Gaze deeply into unknown regions using the power of the moon. ![Example](./media/simple_rg_v1.gif) +## Installation + +```vim +Plug 'nvim-lua/plenary.nvim' +Plug 'nvim-lua/telescope.nvim' +``` + ## Goals @@ -26,6 +33,9 @@ Gaze deeply into unknown regions using the power of the moon. - sometimes built-in - sometimes a lua callback + +As an example, you could pipe your inputs into fzf, and then it can sort them for you. + fzf: - have a list of inputs - i have a prompt/things people typed diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index e5a601e..10716d0 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -29,6 +29,7 @@ function Finder:new(opts) -- list -- ... return setmetatable({ + results = opts.results, fn_command = opts.fn_command, static = opts.static, state = {}, @@ -46,6 +47,16 @@ end -- 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 + if (self.state.job_id or 0) > 0 then vim.fn.jobstop(self.job_id) end diff --git a/lua/telescope/log.lua b/lua/telescope/log.lua index fb3d712..a9bc2b0 100644 --- a/lua/telescope/log.lua +++ b/lua/telescope/log.lua @@ -1,83 +1,4 @@ --- https://raw.githubusercontent.com/rxi/log.lua/master/log.lua --- log.lua --- --- Copyright (c) 2016 rxi --- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. --- - -local log = { _version = "0.1.0" } - -log.usecolor = true -log.outfile = vim.fn.stdpath('data') .. '/telescope.log' -log.console = false -log.level = "trace" - - -local modes = { - { name = "trace", color = "\27[34m", }, - { name = "debug", color = "\27[36m", }, - { name = "info", color = "\27[32m", }, - { name = "warn", color = "\27[33m", }, - { name = "error", color = "\27[31m", }, - { name = "fatal", color = "\27[35m", }, +return require('plenary.log').new { + plugin = 'telescope', + level = 'debug', } - - -local levels = {} -for i, v in ipairs(modes) do - levels[v.name] = i -end - - -local round = function(x, increment) - increment = increment or 1 - x = x / increment - return (x > 0 and math.floor(x + .5) or math.ceil(x - .5)) * increment -end - -for i, x in ipairs(modes) do - local nameupper = x.name:upper() - log[x.name] = function(...) - -- Return early if we're below the log level - if i < levels[log.level] then - return - end - - local passed = {...} - local fmt = table.remove(passed, 1) - local inspected = {} - for _, v in ipairs(passed) do - table.insert(inspected, vim.inspect(v)) - end - local msg = string.format(fmt, unpack(inspected)) - local info = debug.getinfo(2, "Sl") - local lineinfo = info.short_src .. ":" .. info.currentline - - -- Output to console - if log.console then - print(string.format("%s[%-6s%s]%s %s: %s", - log.usecolor and x.color or "", - nameupper, - os.date("%H:%M:%S"), - log.usecolor and "\27[0m" or "", - lineinfo, - msg)) - end - - -- Output to log file - if log.outfile then - local fp = io.open(log.outfile, "a") - local str = string.format("[%-6s%s] %s: %s\n", - nameupper, os.date(), lineinfo, msg) - fp:write(str) - fp:close() - end - - end -end - -log.info("Logger Succesfully Loaded") - -return log diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index db5ec7d..f2acfac 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -1,11 +1,7 @@ local a = vim.api -local fun = require('fun') local popup = require('popup') -local zip = fun.zip -local tomap = fun.tomap - local log = require('telescope.log') local mappings = require('telescope.mappings') local state = require('telescope.state') @@ -13,6 +9,8 @@ local utils = require('telescope.utils') local pickers = {} +--- Picker is the main UI that shows up to interact w/ your results. +-- Takes a filter & a previewr local Picker = {} Picker.__index = Picker @@ -133,98 +131,77 @@ function Picker:find(opts) 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. - - -- Sorted table by scores. - -- Lowest score gets lowest index. - self.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. - local replace_line = function(score, row, line) - log.trace("Replacing @ %s w/ text '%s' (%s)", row, line, score) - vim.api.nvim_buf_set_lines(results_bufnr, row, row + 1, false, {line}) - end - - local insert_line = function(score, row, line) - log.trace("Inserting @ %s w/ text '%s' (%s)", row, line, score) - vim.api.nvim_buf_set_lines(results_bufnr, row, row, false, {line}) - end + local line_manager = pickers.line_manager( + self.max_results, + function(index, line) + local row = self.max_results - index + 1 + log.trace("Setting row", row, "with value", line) + vim.api.nvim_buf_set_lines(results_bufnr, row, row + 1, false, {line}) + end + ) local process_result = function(line) if vim.trim(line) == "" then return end + log.trace("Processing result... ", line) + + local sort_score = 0 if sorter then - local sort_score = sorter:score(prompt, line) + sort_score = sorter:score(prompt, line) if sort_score == -1 then + log.trace("Filtering out result: ", line) return end - - -- { 7, 3, 1, 1 } - -- 2 - for row, row_score in utils.reversed_ipairs(self.line_scores) do - if row_score > sort_score then - -- Insert line at row - insert_line(sort_score, self.max_results - row, line) - - -- Insert current score in the table - table.insert(self.line_scores, row + 1, sort_score) - - -- All done :) - return - end - - -- Don't keep inserting stuff - if row > self.max_results then - return - end - end - - -- Worst score so far, so add to end - - -- example: 5 max results, 8 - local worst_line = self.max_results - #self.line_scores - replace_line(sort_score, worst_line, line) - table.insert(self.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 + + line_manager:add_result(sort_score, line) end local process_complete = function() - local worst_line = self.max_results - #self.line_scores - local empty_lines = {} - for _ = 1, worst_line do table.insert(empty_lines, "") end - vim.api.nvim_buf_set_lines(results_bufnr, 0, worst_line, false, empty_lines) + local worst_line = self.max_results - line_manager.num_results() + local empty_lines = utils.repeated_table(worst_line, "") + -- vim.api.nvim_buf_set_lines(results_bufnr, 0, worst_line + 1, false, empty_lines) - log.info("Worst Line after process_complete: %s", worst_line) - log.trace("%s", tomap(zip( - a.nvim_buf_get_lines(results_bufnr, worst_line, self.max_results, false), - self.line_scores - ))) + log.debug("Worst Line after process_complete: %s", worst_line, results_bufnr) + + -- local fun = require('fun') + -- local zip = fun.zip + -- local tomap = fun.tomap + + -- log.trace("%s", tomap(zip( + -- a.nvim_buf_get_lines(results_bufnr, worst_line, self.max_results, false), + -- self.line_scores + -- ))) end - pcall(function() + local ok, msg = pcall(function() return finder(prompt, process_result, process_complete) end) + + if not ok then + log.warn("Failed with msg: ", msg) + end end - -- Call this once to pre-populate if it makes sense - -- vim.schedule_wrap(on_lines(nil, nil, nil, 0, 1)) + -- TODO: Uncomment + vim.schedule(function() + on_lines(nil, nil, nil, 0, 1) + end) -- Register attach vim.api.nvim_buf_attach(prompt_bufnr, true, { @@ -359,4 +336,82 @@ pickers.new = function(...) return Picker:new(...) end +-- TODO: We should consider adding `process_bulk` or `bulk_line_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...") + + -- state contains list of + -- { + -- score = ... + -- line = ... + -- metadata ? ... + -- } + local state = {} + + set_line = set_line or function() end + + return setmetatable({ + add_result = function(self, score, line) + score = score or 0 + + for index, item in ipairs(state) do + if item.score > score then + return self:insert(index, { + score = score, + line = line, + }) + end + + -- Don't add results that are too bad. + if index >= max_results then + return self + end + end + + return self:insert({ + score = score, + line = line, + }) + end, + + insert = function(self, index, item) + if item == nil then + item = index + index = #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 + repeat + next_item = state[index] + + set_line(index, item.line) + state[index] = item + + index = index + 1 + item = next_item + until not next_item + end, + + num_results = function() + return #state + end, + + _get_state = function() + return state + end, + }, { + -- insert = + + -- __index = function(_, line) + -- end, + + -- __newindex = function(_, index, line) + -- end, + }) +end + + return pickers diff --git a/lua/telescope/previewers.lua b/lua/telescope/previewers.lua index 31ea364..ee99d11 100644 --- a/lua/telescope/previewers.lua +++ b/lua/telescope/previewers.lua @@ -29,7 +29,7 @@ previewers.vim_buffer = previewers.new { end local file_name = vim.split(line, ":")[1] - log.info("Previewing File: %s", file_name) + log.trace("Previewing File: %s", file_name) -- vim.fn.termopen( -- string.format("bat --color=always --style=grid %s"), diff --git a/lua/telescope/sorters.lua b/lua/telescope/sorters.lua index c97a1a9..0299c91 100644 --- a/lua/telescope/sorters.lua +++ b/lua/telescope/sorters.lua @@ -1,3 +1,4 @@ +local log = require('telescope.log') local util = require('telescope.utils') local sorters = {} @@ -12,7 +13,7 @@ Sorter.__index = Sorter --- --- 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: +--- @field scoring_function function Function that has the interface: -- (sorter, prompt, line): number function Sorter:new(opts) opts = opts or {} @@ -59,4 +60,14 @@ sorters.get_ngram_sorter = function() } end +sorters.get_levenshtein_sorter = function() + return Sorter:new { + scoring_function = function(_, prompt, line) + local result = require('telescope.algos.string_distance')(prompt, line) + log.info("Sorting result for", prompt, line, " = ", result) + return result + end + } +end + return sorters diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index e758ed0..7ddc428 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -19,6 +19,15 @@ utils.default_table_mt = { end } +utils.repeated_table = function(n, val) + local empty_lines = {} + for _ = 1, n do + table.insert(empty_lines, val) + end + return empty_lines +end + + local NGram = {} NGram.__index = NGram diff --git a/lua/tests/telescope_spec.lua b/lua/tests/telescope_spec.lua index 692f469..c671579 100644 --- a/lua/tests/telescope_spec.lua +++ b/lua/tests/telescope_spec.lua @@ -1,5 +1,6 @@ require('plenary.test_harness'):setup_busted() +local pickers = require('telescope.pickers') local utils = require('telescope.utils') --[[ @@ -13,6 +14,81 @@ describe('Picker', function() end) end) + describe('process_result', function() + it('works with one entry', function() + local lines_manager = pickers.line_manager(5, nil) + + lines_manager:add_result(1, "hello") + + assert.are.same(1, lines_manager:_get_state()[1].score) + end) + + it('works with two entries', function() + local lines_manager = pickers.line_manager(5, nil) + + lines_manager:add_result(1, "hello") + lines_manager:add_result(2, "later") + + assert.are.same("hello", lines_manager:_get_state()[1].line) + assert.are.same("later", lines_manager:_get_state()[2].line) + 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) + + assert(called_count == 0) + lines_manager:add_result(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) + + assert(called_count == 0) + lines_manager:add_result(1, "hello") + lines_manager:add_result(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") + + assert.are.same("better result", lines_manager:_get_state()[1].line) + assert.are.same("worse result", lines_manager:_get_state()[2].line) + + -- once to insert "worse" + -- once to insert "better" + -- and then to move "worse" + assert.are.same(3, called_count) + end) + + 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") + + assert.are.same("better result", lines_manager:_get_state()[1].line) + + -- once to insert "worse" + -- once to insert "better" + -- and then to move "worse" + assert.are.same(1, called_count) + end) + + -- 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) + + -- lines_manager:add_result(nil, + -- end) + end) + describe('ngrams', function() it('should capture intself in the ngram', function() local n = utils.new_ngram() diff --git a/scratch/simplest_test.lua b/scratch/simplest_test.lua new file mode 100644 index 0000000..be7a4fb --- /dev/null +++ b/scratch/simplest_test.lua @@ -0,0 +1,31 @@ +require('plenary.reload').reload_module('telescope') + +local telescope = require('telescope') + +-- What is a finder? +-- Finders return a list of stuff that you want to fuzzy look through. +-- Finders can be static or not. +-- Static finders just return a list that never changes +-- Otherwise they return a new list on each input, you should handle them async. +local file_finder = telescope.finders.new { + static = true, + + fn_command = function() return 'git ls-files' end, +} + +local file_previewer = telescope.previewers.vim_buffer_or_bat + +local file_picker = telescope.pickers.new { + previewer = file_previewer +} + +local file_sorter = telescope.sorters.get_ngram_sorter() +-- local file_sorter = require('telescope.sorters').get_levenshtein_sorter() + +file_picker:find { + prompt = 'Simple File', + finder = file_finder, + sorter = file_sorter, +} + +local x = function() end