diff --git a/README.md b/README.md index 7b24075..2884693 100644 --- a/README.md +++ b/README.md @@ -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. `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 @@ -103,8 +103,9 @@ require("clipring").setup({ reorder_down_mapping = "", -- picker: move entry down in history (false to disable) reorder_up_mapping = "", -- 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 }) diff --git a/lua/clipring/config.lua b/lua/clipring/config.lua index 0c5bf04..af6f358 100644 --- a/lua/clipring/config.lua +++ b/lua/clipring/config.lua @@ -11,8 +11,9 @@ local M = {} ---@field reorder_down_mapping string|false|nil move selected entry down in picker (default ``) ---@field reorder_up_mapping string|false|nil move selected entry up in picker (default ``) ---@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 @@ -27,8 +28,9 @@ M.defaults = { reorder_down_mapping = "", reorder_up_mapping = "", 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, } diff --git a/lua/clipring/ui.lua b/lua/clipring/ui.lua index 7077696..8d4efcf 100644 --- a/lua/clipring/ui.lua +++ b/lua/clipring/ui.lua @@ -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") @@ -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) @@ -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 @@ -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() @@ -351,19 +361,27 @@ 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, @@ -371,19 +389,49 @@ local function picker_layout() 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 @@ -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 @@ -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, @@ -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() diff --git a/tests/ui_spec.lua b/tests/ui_spec.lua index b36cbc0..41e7fd6 100644 --- a/tests/ui_spec.lua +++ b/tests/ui_spec.lua @@ -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) @@ -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) @@ -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)