From 0dfe9e1edfa795a6e36af8f6741ffbbd2697a8c5 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Sat, 18 Nov 2023 18:46:16 +0100 Subject: [PATCH] feat: Support Line Column in file pickers Original issue: When selecting files in telescope iti is impossible to enter paths from compiler that includes cursor locations, you need to clear it from the path. This commit fixes the problem by stripping out the location from path (important: only for file pickes) and using this location to set the cursor when openning the file and highlight the line or poisition in the previewer. This feature requires a new option for a picker, whichi for now is basically enabling location stripping but it is helpful for any file picker builtin or external one. By default equals `false` becuase most of pickers like live grep, current buffer fuzy are messed up with locations stripping. It is only useful for file searches. Added test suite that covers alghoritm of stripping the location to the `utils_spec.lua` --- lua/telescope/builtin/__files.lua | 1 + lua/telescope/builtin/__git.lua | 1 + lua/telescope/pickers.lua | 27 +++++++++++ lua/telescope/previewers/buffer_previewer.lua | 29 +++++++++++- lua/telescope/utils.lua | 41 +++++++++++++++++ lua/tests/automated/utils_spec.lua | 45 +++++++++++++++++++ 6 files changed, 142 insertions(+), 2 deletions(-) diff --git a/lua/telescope/builtin/__files.lua b/lua/telescope/builtin/__files.lua index fc91fee..9eb5b5d 100644 --- a/lua/telescope/builtin/__files.lua +++ b/lua/telescope/builtin/__files.lua @@ -384,6 +384,7 @@ files.find_files = function(opts) pickers .new(opts, { prompt_title = "Find Files", + files_picker = true, finder = finders.new_oneshot_job(find_command, opts), previewer = conf.file_previewer(opts), sorter = conf.file_sorter(opts), diff --git a/lua/telescope/builtin/__git.lua b/lua/telescope/builtin/__git.lua index a50434f..620884f 100644 --- a/lua/telescope/builtin/__git.lua +++ b/lua/telescope/builtin/__git.lua @@ -46,6 +46,7 @@ git.files = function(opts) pickers .new(opts, { prompt_title = "Git Files", + files_picker = true, finder = finders.new_oneshot_job( vim.tbl_flatten { opts.git_command, diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index e28a451..cd596ed 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -319,6 +319,8 @@ function Picker:new(opts) cache_picker = config.resolve_table_opts(opts.cache_picker, vim.deepcopy(config.values.cache_picker)), __scrolling_limit = tonumber(vim.F.if_nil(opts.temp__scrolling_limit, 250)), + + allow_location_input = opts.files_picker or false, }, self) obj.create_layout = opts.create_layout or config.values.create_layout or default_create_layout @@ -626,6 +628,24 @@ function Picker:find() local start_time = vim.loop.hrtime() local prompt = self:_get_next_filtered_prompt() + if self.allow_location_input == true then + local filename, line_number, column_number = utils.separate_file_path_location(prompt) + + if line_number or column_number then + state.set_global_key("prompt_location", { row = line_number, col = column_number }) + self:refresh_previewer() + elseif state.get_global_key "prompt_location" then + state.set_global_key("prompt_location", nil) + self:refresh_previewer() + end + + -- it is important to continue behaving as if there is no location in prompt + prompt = filename + elseif state.get_global_key "prompt_location" then + -- in case new picker that does not support locations is opened clear the location + -- without refreshing previewer + state.set_global_key("prompt_location", nil) + end -- TODO: Entry manager should have a "bulk" setter. This can prevent a lot of redraws from display if self.cache_picker == false or self.cache_picker.is_cached ~= true then @@ -1046,6 +1066,13 @@ function Picker:set_selection(row) end local entry = self.manager:get_entry(self:get_index(row)) + + local prompt_location = state.get_global_key "prompt_location" + if entry and prompt_location then + entry.lnum = prompt_location.row or 0 + entry.col = prompt_location.col or 0 + end + state.set_global_key("selected_entry", entry) if not entry then diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index 14d4241..be865dd 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -4,6 +4,7 @@ local utils = require "telescope.utils" local putils = require "telescope.previewers.utils" local Previewer = require "telescope.previewers.previewer" local conf = require("telescope.config").values +local global_state = require "telescope.state" local pscan = require "plenary.scandir" @@ -345,8 +346,6 @@ previewers.new_buffer_previewer = function(opts) local old_bufs = {} local bufname_table = {} - - local global_state = require "telescope.state" local preview_window_id local function get_bufnr(self) @@ -489,6 +488,29 @@ end previewers.cat = defaulter(function(opts) opts = opts or {} local cwd = opts.cwd or vim.loop.cwd() + local function jump_to_line(bufnr, winid) + pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_previewer, 0, -1) + local location = global_state.get_global_key "prompt_location" + + if location and location.row > 0 then + local highlight_range = location.col and location.col > 0 and { location.col - 1, location.col } or { 0, -1 } + + pcall( + vim.api.nvim_buf_add_highlight, + bufnr, + ns_previewer, + "TelescopePreviewLine", + location.row - 1, + highlight_range[1], + highlight_range[2] + ) + + pcall(vim.api.nvim_win_set_cursor, winid, { location.row, location.col }) + vim.api.nvim_buf_call(bufnr, function() + vim.cmd "norm! zz" + end) + end + end return previewers.new_buffer_previewer { title = "File Preview", dyn_title = function(_, entry) @@ -509,6 +531,9 @@ previewers.cat = defaulter(function(opts) winid = self.state.winid, preview = opts.preview, file_encoding = opts.file_encoding, + callback = function(bufnr) + jump_to_line(bufnr, self.state.winid) + end, }) end, } diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index cb87c33..9b48ca4 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -567,4 +567,45 @@ utils.list_find = function(func, list) end end +--- Takes the path and parses optional cursor location `$file:$line:$column` +--- If line or column not present `0` returne returned. +--- @param path string +utils.separate_file_path_location = function(path) + local location_numbers = {} + -- Split the 2 last `:` separated parts and if they are numbers treat + -- as line and columnd numbers + for i = #path, 1, -1 do + if path:sub(i, i) == ":" then + -- If this is the last `:` we ignore it for a case when user + -- just entered it with intention to enter a line or column number + -- to not display "No results" immediately. + if i == #path then + path = path:sub(1, i - 1) + else + local location_value = tonumber(path:sub(i + 1)) + if location_value then + table.insert(location_numbers, location_value) + path = path:sub(1, i - 1) + + if #location_numbers == 2 then + -- There couldn't be more than 2 : separated number + break + end + end + end + end + end + + if #location_numbers == 2 then + -- because of the reverse the line number will be second + return path, location_numbers[2], location_numbers[1] + end + + if #location_numbers == 1 then + return path, location_numbers[1], 0 + end + + return path, 0, 0 +end + return utils diff --git a/lua/tests/automated/utils_spec.lua b/lua/tests/automated/utils_spec.lua index 5edf356..608f9a1 100644 --- a/lua/tests/automated/utils_spec.lua +++ b/lua/tests/automated/utils_spec.lua @@ -78,3 +78,48 @@ describe("is_uri", function() end end) end) + +describe("separates file path location", function() + local suites = { + { + input = "file.txt:12:4", + file = "file.txt", + row = 12, + col = 4, + }, + { + input = "file.txt:12", + file = "file.txt", + row = 12, + col = 0, + }, + { + input = "file:12:4", + file = "file", + row = 12, + col = 4, + }, + { + input = "file:12:", + file = "file", + row = 12, + col = 0, + }, + { + input = "file:", + file = "file", + row = 0, + col = 0, + }, + } + + for _, suite in ipairs(suites) do + it("separtates file path for " .. suite.input, function() + local file, row, col = utils.separate_file_path_location(suite.input) + + assert.are.equal(file, suite.file) + assert.are.equal(row, suite.row) + assert.are.equal(col, suite.col) + end) + end +end)