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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ With a minimal `lazy.nvim` / `packer.nvim` setup, Neovim loads the plugin from `
| `:ClipRing` | Always available (no keymap required) |
| Your `open_mapping` | After you set one in `setup()` (e.g. `<leader>y`) |

The picker opens as two side-by-side floats: a **history list** (newest first, one line per entry with register type `c` / `l` / `b`) and a **preview pane** showing the selected yank with real line breaks (up to `preview_max_lines`).
The picker opens as two side-by-side floats: a wide **history list** (newest first, one line per entry with register type `c` / `l` / `b`) and a narrower **preview pane** showing the selected yank with real line breaks (up to `preview_max_lines`).

### Inside the picker

Expand Down Expand Up @@ -103,8 +103,9 @@ require("clipring").setup({
reorder_down_mapping = "<C-j>", -- picker: move entry down in history (false to disable)
reorder_up_mapping = "<C-k>", -- picker: move entry up in history (false to disable)
copy_mapping = "y", -- picker: copy to system clipboard (false to disable)
picker_width = 90, -- total width of list + preview (columns)
list_width = 34, -- width of the history list
picker_width = 80, -- total inner width of list + preview (0 = editor width minus margin)
list_width = 0, -- history list columns (0 = remaining width after preview)
preview_max_width = 80, -- max width of the preview pane (columns)
picker_max_height = 18, -- max height of both floats (lines)
preview_max_lines = 16, -- max lines shown per entry in the preview pane
})
Expand Down
10 changes: 6 additions & 4 deletions lua/clipring/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ local M = {}
---@field reorder_down_mapping string|false|nil move selected entry down in picker (default `<C-j>`)
---@field reorder_up_mapping string|false|nil move selected entry up in picker (default `<C-k>`)
---@field copy_mapping string|false|nil copy selected entry to system clipboard in picker (default `y`)
---@field picker_width number total width of list + preview floats
---@field list_width number width of the history list (columns)
---@field picker_width number total inner width of list + preview (`0` = nearly full editor width)
---@field list_width number width of the history list (columns; `0` = fill space not used by preview)
---@field preview_max_width number max width of the preview pane (columns)
---@field picker_max_height number max height of picker windows (lines)
---@field preview_max_lines number max lines shown in the preview pane for one entry

Expand All @@ -27,8 +28,9 @@ M.defaults = {
reorder_down_mapping = "<C-j>",
reorder_up_mapping = "<C-k>",
copy_mapping = "y",
picker_width = 90,
list_width = 34,
picker_width = 80,
list_width = 0,
preview_max_width = 80,
picker_max_height = 18,
preview_max_lines = 16,
}
Expand Down
94 changes: 72 additions & 22 deletions lua/clipring/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ local state = {
opener_mode = "n",
visual_marks = nil,
index = 1,
list_col_width = 50,
}

local ns = vim.api.nvim_create_namespace("ClipRing")
Expand All @@ -29,17 +30,22 @@ local function entry_kind(entry)
return "c"
end

--- One-line label for the history list.
local function list_label(entry)
--- One-line label for the history list (truncated to fit the list pane).
local function list_label(entry, list_cols)
local opts = config.get()
local text = table.concat(entry.lines, " "):gsub("\t", " "):gsub("%s+", " ")
if #text > opts.preview_length then
text = text:sub(1, opts.preview_length - 3) .. "..."
local header = string.format("[%s] ", entry_kind(entry))
local prefix_cols = 2 -- "▸ " or " "
local budget = (list_cols or state.list_col_width) - prefix_cols - vim.fn.strdisplaywidth(header)
budget = math.min(budget, opts.preview_length)
budget = math.max(budget, 8)
if vim.fn.strdisplaywidth(text) > budget then
text = vim.fn.strcharpart(text, 0, budget - 3) .. "..."
end
if text == "" then
text = "(empty)"
end
return string.format("[%s] %s", entry_kind(entry), text)
return header .. text
end

local function set_buf_lines(buf, lines)
Expand Down Expand Up @@ -67,7 +73,7 @@ local function refresh_list_buffer()
end
for i, entry in ipairs(all) do
local prefix = (i == state.index) and "▸ " or " "
table.insert(lines, prefix .. list_label(entry))
table.insert(lines, prefix .. list_label(entry, state.list_col_width))
end
end

Expand Down Expand Up @@ -95,7 +101,11 @@ local function preview_lines_for_entry(entry)
table.insert(truncated, string.format("… (%d more lines)", #lines - max_lines))
lines = truncated
end
return lines
local padded = {}
for i, line in ipairs(lines) do
padded[i] = " " .. line
end
return padded
end

local function refresh_preview_buffer()
Expand Down Expand Up @@ -351,39 +361,77 @@ local function picker_layout()
local height = math.max(list_lines, preview_lines, 3)
height = math.min(height, opts.picker_max_height, vim.o.lines - 4)

local total_width = math.min(opts.picker_width, vim.o.columns - 4)
local list_width = math.min(opts.list_width, total_width - 20)
if list_width < 24 then
list_width = math.min(24, total_width - 20)
end
local preview_width = total_width - list_width
if preview_width < 20 then
preview_width = 20
local margin = 8
local total_width = opts.picker_width > 0 and opts.picker_width or (vim.o.columns - margin)
total_width = math.min(total_width, vim.o.columns - margin)
total_width = math.max(total_width, 60)

local preview_cap = math.max(20, opts.preview_max_width or 80)
preview_cap = math.min(preview_cap, total_width - 36)

local preview_width = preview_cap
local list_width
if opts.list_width > 0 then
list_width = math.min(opts.list_width, total_width - preview_width)
else
list_width = total_width - preview_width
end
list_width = math.max(list_width, 36)

local float_gap = 1
local footprint = (list_width + 2) + float_gap + (preview_width + 2)
local row = math.max(0, math.floor((vim.o.lines - height) / 2))
local col = math.max(0, math.floor((vim.o.columns - total_width) / 2))
local col = math.max(0, math.floor((vim.o.columns - footprint) / 2))

return {
row = row,
col = col,
height = height,
list_width = list_width,
preview_width = preview_width,
preview_col = col + list_width + 2 + float_gap,
float_gap = float_gap,
}
end

local function apply_picker_layout(layout)
if not state.list_win or not vim.api.nvim_win_is_valid(state.list_win) then
return
end
state.list_col_width = layout.list_width
vim.api.nvim_win_set_config(state.list_win, {
relative = "editor",
width = layout.list_width,
height = layout.height,
row = layout.row,
col = layout.col,
})
if state.preview_win and vim.api.nvim_win_is_valid(state.preview_win) then
vim.api.nvim_win_set_config(state.preview_win, {
relative = "editor",
width = layout.preview_width,
height = layout.height,
row = layout.row,
col = layout.preview_col,
})
end
end

local float_opts = {
relative = "editor",
style = "minimal",
border = "rounded",
}

local function apply_float_winhl(win)
if vim.api.nvim_win_is_valid(win) then
vim.wo[win].winhl = "Normal:NormalFloat,FloatBorder:FloatBorder,CursorLine:Visual"
local function configure_float_win(win)
if not vim.api.nvim_win_is_valid(win) then
return
end
vim.wo[win].winhl = "Normal:NormalFloat,FloatBorder:FloatBorder,CursorLine:Visual"
vim.wo[win].number = false
vim.wo[win].relativenumber = false
vim.wo[win].signcolumn = "no"
vim.wo[win].foldcolumn = "0"
end

---@param opts table|nil
Expand All @@ -392,6 +440,7 @@ function M.open(opts)
opts = opts or {}

if state.list_win and vim.api.nvim_win_is_valid(state.list_win) then
apply_picker_layout(picker_layout())
refresh_buffers()
focus_list_normal()
return
Expand All @@ -414,6 +463,7 @@ function M.open(opts)
state.preview_buf = create_readonly_buf("clipring://preview", "clipring_preview")

local layout = picker_layout()
state.list_col_width = layout.list_width

state.list_win = vim.api.nvim_open_win(state.list_buf, true, vim.tbl_extend("force", float_opts, {
width = layout.list_width,
Expand All @@ -428,11 +478,11 @@ function M.open(opts)
width = layout.preview_width,
height = layout.height,
row = layout.row,
col = layout.col + layout.list_width,
col = layout.preview_col,
}))

apply_float_winhl(state.list_win)
apply_float_winhl(state.preview_win)
configure_float_win(state.list_win)
configure_float_win(state.preview_win)

attach_keymaps()
refresh_buffers()
Expand Down
10 changes: 5 additions & 5 deletions tests/ui_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe("clipring.ui", function()
ui.open()
local preview_buf = h.find_clipring_preview_buf()
assert.is_not_nil(preview_buf)
assert.same({ "alpha", "beta", "gamma" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false))
assert.same({ " alpha", " beta", " gamma" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false))
ui.close()
end)

Expand All @@ -96,9 +96,9 @@ describe("clipring.ui", function()
ring.add({ "solo" }, "v")
ui.open()
local preview_buf = h.find_clipring_preview_buf()
assert.same({ "solo" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false))
assert.same({ " solo" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false))
feed_clipring("j")
assert.same({ "one", "two" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false))
assert.same({ " one", " two" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false))
ui.close()
end)

Expand All @@ -115,8 +115,8 @@ describe("clipring.ui", function()
ui.open()
local preview_buf = h.find_clipring_preview_buf()
local lines = vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false)
assert.are.equal("a", lines[1])
assert.are.equal("b", lines[2])
assert.are.equal(" a", lines[1])
assert.are.equal(" b", lines[2])
assert.matches("2 more lines", lines[3])
ui.close()
end)
Expand Down