Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions doc/peekstack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,17 @@ Custom action:
>lua
require("fzf-lua").live_grep({
actions = {
["default"] = function(selected)
require("peekstack.extensions.fzf_lua").actions.push(selected, {
["default"] = function(selected, opts)
require("peekstack.extensions.fzf_lua").actions.push(selected, opts, {
provider = "my_grep",
})
end,
},
})
<
Pass the picker `opts` fzf-lua gives the action so relative entries are
resolved against the picker `cwd`. The wrappers above do this for you and
replace only the confirm action, so other `actions` you pass keep working.

TELESCOPE ~
*peekstack-extensions-telescope*
Expand All @@ -246,6 +249,8 @@ Custom action:
end,
})
<
The `push_*` pickers map `<CR>` and then run the `attach_mappings` you pass,
so your own mappings are kept and may override `<CR>`.

GENERIC PUSH ~
*peekstack-extensions-push-entry*
Expand Down
50 changes: 37 additions & 13 deletions lua/peekstack/extensions/fzf_lua.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,34 @@ local notify = require("peekstack.util.notify")

local M = {}

---@param value integer?
---@return integer?
local function positive(value)
return value and value > 0 and value or nil
end

---Push the selected fzf-lua entry onto the stack.
---`fzf_opts` are the picker opts fzf-lua passes to the action; they carry the
---picker cwd that relative entries are resolved against.
---@param selected string[]
---@param opts? table
local function push_action(selected, opts)
---@param fzf_opts? table
---@param push_opts? { provider?: string, mode?: string }
local function push_action(selected, fzf_opts, push_opts)
if not selected or not selected[1] then
return
end
local ok, fzf = pcall(require, "fzf-lua")
if not ok then
return
end
local entry = fzf.path.entry_to_file(selected[1])
local entry = fzf.path.entry_to_file(selected[1], fzf_opts)
if entry then
-- fzf-lua reports 0 for a missing line or column.
ext.push_entry({
filename = entry.path,
lnum = entry.line,
col = entry.col,
}, opts)
lnum = positive(entry.line),
col = positive(entry.col),
}, push_opts)
end
end

Expand All @@ -40,14 +51,27 @@ local function open_picker(fzf_picker, provider, opts)
end

local push_opts = { provider = provider, mode = opts.mode }
-- Replace only the confirm action so the caller's other actions keep working.
-- fzf-lua maps "default" to "enter" and lets it win over an "enter" entry.
local confirm = {
["default"] = function(selected, picker_opts)
push_action(selected, picker_opts, push_opts)
end,
}
local user_actions = opts.actions
local actions
if type(user_actions) == "function" then
-- fzf-lua also accepts actions as a function of the picker opts.
actions = function(...)
return vim.tbl_extend("force", user_actions(...) or {}, confirm)
end
else
actions = vim.tbl_extend("force", user_actions or {}, confirm)
end
local fzf_opts = vim.tbl_extend("force", opts, { actions = actions })
fzf_opts.mode = nil

fn(vim.tbl_extend("force", opts, {
actions = {
["default"] = function(selected)
push_action(selected, push_opts)
end,
},
}))
fn(fzf_opts)
end

function M.push_file(opts)
Expand Down
35 changes: 26 additions & 9 deletions lua/peekstack/providers/grep.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ local function format_failure_message(stderr)
return "rg failed: " .. message
end

---@param path string
---@param cwd string
---@return string
local function resolve_path(path, cwd)
if vim.fn.isabsolutepath(path) ~= 1 then
path = vim.fs.joinpath(cwd, path)
end
return vim.fs.normalize(path, { expand_env = false })
end

---@param line string
---@param cwd string
---@return string?, integer?, integer?, string?
local function parse_rg_line(line)
local function parse_rg_line(line, cwd)
local candidates = {}
local search_from = 1

Expand All @@ -60,25 +71,27 @@ local function parse_rg_line(line)

for i = #candidates, 1, -1 do
local candidate = candidates[i]
local resolved = vim.fn.fnamemodify(vim.fn.expand(candidate.path), ":p")
local resolved = resolve_path(candidate.path, cwd)
local stat = vim.uv.fs_stat(resolved)
if stat and stat.type == "file" then
return candidate.path, candidate.lnum, candidate.col, candidate.text
return resolved, candidate.lnum, candidate.col, candidate.text
end
end

local candidate = candidates[1]
return candidate.path, candidate.lnum, candidate.col, candidate.text
return resolve_path(candidate.path, cwd), candidate.lnum, candidate.col, candidate.text
end

---Parse `rg --vimgrep` output, resolving relative paths against the directory rg ran in.
---@param output string
---@param cwd string
---@return PeekstackLocation[]
local function parse_rg_output(output)
local function parse_rg_output(output, cwd)
local items = {}
for _, line in ipairs(vim.split(output, "\n", { trimempty = true })) do
local path, lnum, col, text = parse_rg_line(line)
local path, lnum, col, text = parse_rg_line(line, cwd)
if path and lnum and col then
local uri = fs.fname_to_uri(vim.fn.fnamemodify(path, ":p"))
local uri = fs.fname_to_uri(path)
local loc = location.normalize({
uri = uri,
range = {
Expand All @@ -104,20 +117,24 @@ function M.search(_, cb)
return
end

-- Pin the directory before prompting so a later :cd cannot split the search
-- and the path resolution between two directories.
local cwd = vim.fn.getcwd()

vim.ui.input({ prompt = "rg > " }, function(query)
if not query or query == "" then
cb({})
return
end

vim.system({ "rg", "--vimgrep", "--max-count=1000", "--", query }, { text = true }, function(result)
vim.system({ "rg", "--vimgrep", "--max-count=1000", "--", query }, { cwd = cwd, text = true }, function(result)
vim.schedule(function()
if result.code ~= 0 and result.code ~= 1 then
notify.warn(format_failure_message(result.stderr))
cb({})
return
end
cb(parse_rg_output(result.stdout or ""))
cb(parse_rg_output(result.stdout or "", cwd))
end)
end)
end)
Expand Down
68 changes: 47 additions & 21 deletions lua/peekstack/ui/diagnostics.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,43 @@ local function is_diagnostic_location(location)
return type(location.provider) == "string" and location.provider:match("^diagnostics%.") ~= nil
end

---@param bufnr integer
---@param row integer
---@return integer
local function row_length(bufnr, row)
return #(vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or "")
end

---Map a diagnostic range onto the popup buffer, keeping only the part inside it.
---A range that starts above or ends below the buffer is clipped to its first or
---last line, and columns are clamped to each line's byte length.
---@param bufnr integer
---@param range table
---@param line_offset integer
---@return { row: integer, col: integer, end_row: integer, end_col: integer }?
local function underline_range(bufnr, range, line_offset)
local last_row = vim.api.nvim_buf_line_count(bufnr) - 1
local start = range.start or {}
local finish = range["end"] or start
local row = (start.line or 0) - line_offset
local end_row = (finish.line or start.line or 0) - line_offset
if end_row < 0 or row > last_row then
return nil
end

local col = row < 0 and 0 or (start.character or 0)
local end_col = end_row > last_row and math.huge or (finish.character or col)
row = math.max(row, 0)
end_row = math.min(end_row, last_row)
col = math.min(math.max(col, 0), row_length(bufnr, row))
end_col = math.min(math.max(end_col, 0), row_length(bufnr, end_row))
-- Columns are only ordered within a single line.
if end_row < row or (end_row == row and end_col < col) then
end_row, end_col = row, col
end
return { row = row, col = col, end_row = end_row, end_col = end_col }
end

---@param popup PeekstackPopupModel
---@return PeekstackDiagnosticExtmarks?
function M.decorate(popup)
Expand All @@ -58,24 +95,16 @@ function M.decorate(popup)
return nil
end

local range = location.range or {}
local start = range.start or {}
local finish = range["end"] or start
local line_offset = popup.line_offset or 0
local line = (start.line or 0) - line_offset
local col = start.character or 0
local end_line = (finish.line or start.line or 0) - line_offset
local end_col = finish.character or col

local line_count = vim.api.nvim_buf_line_count(bufnr)
if line_count == 0 then
return nil
end

line = math.min(math.max(line, 0), line_count - 1)
end_line = math.min(math.max(end_line, line), line_count - 1)
col = math.max(col, 0)
end_col = math.max(end_col, col)
local range = location.range or {}
local line_offset = popup.line_offset or 0
local start_row = ((range.start or {}).line or 0) - line_offset
local line = math.min(math.max(start_row, 0), line_count - 1)
local underline = underline_range(bufnr, range, line_offset)

local ids = {}

Expand All @@ -97,14 +126,11 @@ function M.decorate(popup)
end
end

local underline = severity_hl(location.kind, "DiagnosticUnderline")
if underline ~= "" then
-- end_col may exceed the line length when the popup buffer is truncated;
-- pcall keeps decoration best-effort instead of erroring on the whole popup.
local ok, id = pcall(vim.api.nvim_buf_set_extmark, bufnr, NS, line, col, {
end_row = end_line,
end_col = end_col,
hl_group = underline,
if underline then
local ok, id = pcall(vim.api.nvim_buf_set_extmark, bufnr, NS, underline.row, underline.col, {
end_row = underline.end_row,
end_col = underline.end_col,
hl_group = severity_hl(location.kind, "DiagnosticUnderline"),
})
if ok then
table.insert(ids, id)
Expand Down
9 changes: 7 additions & 2 deletions lua/peekstack/ui/stack_view/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,17 @@ function M.apply(bufnr, old_keys, model, preview_cache)
local start_idx, old_end, new_end = diff_range(old_keys, new_keys)
if start_idx then
local replace = slice_lines(model.lines, start_idx, new_end)
-- Without previous keys the buffer only holds the empty line of a new
-- buffer, so replace it instead of inserting above it.
local old_stop = #old_keys == 0 and -1 or old_end

-- Clear while the old rows are still in place: after the replacement the
-- unchanged suffix may have moved into the old range.
vim.api.nvim_buf_clear_namespace(bufnr, NS, start_idx - 1, old_stop)
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, start_idx - 1, old_end, false, replace)
vim.api.nvim_buf_set_lines(bufnr, start_idx - 1, old_stop, false, replace)
vim.bo[bufnr].modifiable = false

vim.api.nvim_buf_clear_namespace(bufnr, NS, start_idx - 1, old_end)
apply_highlights_in_range(bufnr, model.highlights, model.preview_lines, start_idx, new_end, preview_cache)
end

Expand Down
18 changes: 13 additions & 5 deletions lua/telescope/_extensions/peekstack.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,28 @@ local function open_builtin(builtin_name, provider, opts)

local push_opts = { provider = provider, mode = opts.mode }

fn(vim.tbl_extend("force", opts, {
attach_mappings = function(_, map)
local user_attach = opts.attach_mappings
local picker_opts = vim.tbl_extend("force", opts, {
attach_mappings = function(prompt_bufnr, map)
local actions = require("telescope.actions")
local state = require("telescope.actions.state")
local function on_select(prompt_bufnr)
local function on_select(bufnr)
local entry = state.get_selected_entry()
actions.close(prompt_bufnr)
actions.close(bufnr)
push_from_telescope(entry, push_opts)
end
map("i", "<CR>", on_select)
map("n", "<CR>", on_select)
-- Run the caller's mappings last so they can still override <CR>.
if user_attach then
return user_attach(prompt_bufnr, map)
end
return true
end,
}))
})
picker_opts.mode = nil

fn(picker_opts)
end

--- Generic action for use in custom telescope mappings.
Expand Down
Loading
Loading