diff --git a/README.md b/README.md index e1150e9..66fc5ce 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,21 @@ [![Tests](https://github.com/alexesba/clipring.nvim/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/alexesba/clipring.nvim/actions/workflows/test.yml) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -Minimal yank history for Neovim — a lightweight Lua plugin inspired by YankRing and Windows Clipboard History. No required dependencies. +Minimal yank history for Neovim — a lightweight Lua plugin inspired by YankRing and Windows Clipboard History. No required dependencies — works with any Neovim setup (LazyVim, packer, plain Lua config). Treesitter and which-key are optional extras, not requirements. **Repository:** [github.com/alexesba/clipring.nvim](https://github.com/alexesba/clipring.nvim) +## Screenshots + +![ClipRing picker with history list and syntax-highlighted preview](doc/screenshots/picker-with-preview.png) + +![ClipRing with an empty yank history](doc/screenshots/picker-empty.png) + ## Features - Automatic capture of every yank -- Floating popup history (`:ClipRing`) with a multiline preview pane +- Floating popup history (`:ClipRing`) with an auto-sizing multiline preview pane +- Preview pane shown only when there is content to display; optional syntax highlighting for code - Navigate with `j` / `k`, reorder with `` / ``, paste with ``, copy to the system clipboard with `y`, delete with `dd` - Works from Normal, Insert, and Visual modes - Optional JSON persistence between sessions @@ -56,7 +63,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 when there are yanks to show: a **history list** (height follows entry count) and a **preview pane** that resizes to fit the selected entry. With an empty ring, only the list is shown. +The picker opens as two side-by-side floats when there are yanks to show: a **history list** (height follows entry count) and a **preview pane** that resizes to fit the selected entry. Code yanks are syntax-highlighted when ClipRing can detect a language (markdown ` ```lang ` fences, shebangs, or simple heuristics). With an empty ring, only the list is shown. ### Inside the picker @@ -70,7 +77,7 @@ The picker opens as two side-by-side floats when there are yanks to show: a **hi | `dd` | Delete the selected entry from history | | `q` or `` | Close without pasting | -While the picker is focused, `` does not switch windows or open which-key (close the picker first, like Telescope). Keys apply to the history list; the preview pane is read-only. If you use [which-key.nvim](https://github.com/folke/which-key.nvim), `setup()` disables which-key on the `clipring` and `clipring_preview` filetypes. +While the picker is focused, `` does not switch windows or open which-key (close the picker first, like Telescope). Keys apply to the history list; the preview pane is read-only. If you use [which-key.nvim](https://github.com/folke/which-key.nvim), `setup()` disables which-key on the history list buffer (`clipring` filetype). ### Paste behavior by mode @@ -109,6 +116,7 @@ require("clipring").setup({ picker_width = 80, -- total inner width; 0 = nearly full editor width picker_max_height = 18, -- max height for list and preview preview_max_lines = 16, -- max lines per entry in the preview pane + preview_syntax = true, -- highlight code in the preview when a language is detected }) ``` @@ -120,6 +128,8 @@ Copy uses Neovim’s `+` and `*` registers (and the unnamed `"` register). You n If `` / `` conflict with global maps (e.g. `:move`), use different keys: `reorder_down_mapping = ""`. +**Preview syntax** — when `preview_syntax` is true (default), ClipRing detects a language from markdown ` ```lang ` fences, shebangs, Neovim’s filetype match, or simple heuristics, then highlights with built-in Vim syntax. If Treesitter parsers are installed, highlighting may look better; set `preview_syntax = false` for plain text only. Fence markers are stripped from the preview — only the code body is shown. + ### Advanced ```lua @@ -149,11 +159,14 @@ Set `PLENARY_DIR` if plenary is already on disk: PLENARY_DIR=~/.local/share/nvim/lazy/plenary.nvim ./scripts/run_tests.sh ``` +To regenerate README screenshots, see [doc/screenshots/README.md](doc/screenshots/README.md). + Coverage today: - **ring** — add, dedupe, max size, remove, reorder +- **preview_syntax** — fence stripping, language detection, heuristics - **paste** — visual capture (`v` / `'<`), charwise replace vs append, insert-mode paste at saved cursor -- **ui** — picker from insert, navigation, reorder keys, multiline preview, clipboard copy, which-key / `` behavior +- **ui** — picker from insert, navigation, reorder keys, auto-size layout, conditional preview, multiline preview, syntax highlighting, clipboard copy, which-key / `` behavior - **yank** — `TextYankPost` capture - **setup** — `open_mapping` registration diff --git a/doc/screenshots/README.md b/doc/screenshots/README.md new file mode 100644 index 0000000..5ee0b8c --- /dev/null +++ b/doc/screenshots/README.md @@ -0,0 +1,32 @@ +# README screenshots + +Maintainer tooling for the images linked from the root [README](../../README.md). Not used by the plugin at runtime. + +| File | Description | +|------|-------------| +| `picker-with-preview.png` | History list + syntax-highlighted preview | +| `picker-empty.png` | Empty ring (list only) | + +## Regenerate (macOS) + +Requires [WezTerm](https://wezfurlong.org/wezterm/) and Screen Recording permission for your terminal. + +```bash +./doc/screenshots/capture.sh # both images +./doc/screenshots/capture.sh full # preview shot only +./doc/screenshots/capture.sh empty # empty ring only +``` + +Preview a demo without capturing: + +```bash +./doc/screenshots/open_demo.sh full +``` + +Demos use `nvim --clean` so your personal config does not override the sample yanks. + +On other platforms, open a demo and use your OS screenshot tool: + +```bash +nvim --clean --cmd "cd $(pwd)" --cmd "luafile doc/screenshots/demo.lua" +``` diff --git a/doc/screenshots/capture.sh b/doc/screenshots/capture.sh new file mode 100755 index 0000000..a493581 --- /dev/null +++ b/doc/screenshots/capture.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Maintainer helper: capture README screenshots (macOS + WezTerm). +# Run from your own terminal — not from Cursor's agent shell. +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$DIR/../.." && pwd)" + +NVIM_OPTS=( + --clean + --cmd "set columns=100 lines=32" + --cmd "cd $ROOT" +) + +capture_interactive() { + local demo_lua="$1" + local output="$2" + local label="$3" + local wait="${4:-3}" + + echo "" + echo "==> $label" + echo " Opening demo..." + wezterm start --always-new-process --position 160,90 -- \ + nvim "${NVIM_OPTS[@]}" --cmd "luafile $demo_lua" & + sleep "$wait" + echo " Click the WezTerm window to save: $output" + screencapture -iW "$output" + echo " Saved $output" + sleep 0.5 +} + +case "${1:-all}" in + full|with-preview|preview) + capture_interactive "$DIR/demo.lua" "$DIR/picker-with-preview.png" \ + "Picker with history list + syntax-highlighted preview" 3.5 + ;; + empty) + capture_interactive "$DIR/demo_empty.lua" "$DIR/picker-empty.png" \ + "Empty ring (list only, no preview pane)" 3 + ;; + all) + capture_interactive "$DIR/demo.lua" "$DIR/picker-with-preview.png" \ + "Picker with history list + syntax-highlighted preview" 3.5 + capture_interactive "$DIR/demo_empty.lua" "$DIR/picker-empty.png" \ + "Empty ring (list only, no preview pane)" 3 + ;; + *) + echo "Usage: $0 [all|full|empty]" >&2 + exit 1 + ;; +esac + +echo "" +echo "Done. Screenshots in $DIR" diff --git a/doc/screenshots/demo.lua b/doc/screenshots/demo.lua new file mode 100644 index 0000000..2f3f07e --- /dev/null +++ b/doc/screenshots/demo.lua @@ -0,0 +1,57 @@ +--- Demo buffer + yank history for README screenshots. +--- Usage: nvim --clean --cmd "cd " --cmd "luafile doc/screenshots/demo.lua" + +local root = vim.fn.fnamemodify(vim.fn.getcwd(), ":p"):gsub("/$", "") + +vim.opt.rtp:prepend(root) +vim.opt.termguicolors = true +vim.opt.number = true +vim.opt.signcolumn = "yes" +vim.opt.swapfile = false +vim.cmd("colorscheme default") + +vim.api.nvim_buf_set_lines(0, 0, -1, true, { + 'require("clipring").setup({', + ' open_mapping = "y",', + " persist = true,", + " preview_syntax = true,", + "})", + "", + "-- Yank history opens with :ClipRing", +}) +vim.bo.filetype = "lua" + +require("clipring").setup({ + open_mapping = nil, + persist = false, + preview_syntax = true, + picker_width = 90, + picker_max_height = 16, + preview_max_lines = 12, + preview_length = 72, +}) + +local ring = require("clipring.ring") +ring.clear() +ring.add({ "Hello from ClipRing!" }, "v") +ring.add({ + "class Widget", + " def name", + " 'clipring'", + " end", + "end", +}, "V") +ring.add({ + "```lua", + 'require("clipring").setup({', + ' open_mapping = "y",', + " preview_syntax = true,", + "})", + "```", +}, "V") + +assert(ring.count() == 3, "demo ring should have 3 entries, got " .. ring.count()) + +vim.defer_fn(function() + require("clipring.ui").open() +end, 500) diff --git a/doc/screenshots/demo_empty.lua b/doc/screenshots/demo_empty.lua new file mode 100644 index 0000000..e7dce3c --- /dev/null +++ b/doc/screenshots/demo_empty.lua @@ -0,0 +1,21 @@ +--- Empty ring demo for README screenshots (list only, no preview pane). + +local root = vim.fn.fnamemodify(vim.fn.getcwd(), ":p"):gsub("/$", "") + +vim.opt.rtp:prepend(root) +vim.opt.termguicolors = true +vim.opt.number = true +vim.opt.swapfile = false +vim.cmd("colorscheme default") + +vim.api.nvim_buf_set_lines(0, 0, -1, true, { + "-- Copy something with y, then open ClipRing", + "", +}) +vim.bo.filetype = "lua" + +require("clipring").setup({ open_mapping = nil, persist = false }) + +vim.defer_fn(function() + require("clipring.ui").open() +end, 500) diff --git a/doc/screenshots/open_demo.sh b/doc/screenshots/open_demo.sh new file mode 100755 index 0000000..601af66 --- /dev/null +++ b/doc/screenshots/open_demo.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Open a ClipRing demo in WezTerm for manual screenshots. +# Usage: ./doc/screenshots/open_demo.sh [full|empty] +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$DIR/../.." && pwd)" +MODE="${1:-full}" + +case "$MODE" in + full) + DEMO="$DIR/demo.lua" + ;; + empty) + DEMO="$DIR/demo_empty.lua" + ;; + *) + echo "Usage: $0 [full|empty]" >&2 + exit 1 + ;; +esac + +exec wezterm start --always-new-process --position 160,90 -- \ + nvim --clean --cmd "set columns=100 lines=32" --cmd "cd $ROOT" --cmd "luafile $DEMO" diff --git a/doc/screenshots/picker-empty.png b/doc/screenshots/picker-empty.png new file mode 100644 index 0000000..15d9864 Binary files /dev/null and b/doc/screenshots/picker-empty.png differ diff --git a/doc/screenshots/picker-with-preview.png b/doc/screenshots/picker-with-preview.png new file mode 100644 index 0000000..1122462 Binary files /dev/null and b/doc/screenshots/picker-with-preview.png differ diff --git a/lua/clipring/config.lua b/lua/clipring/config.lua index bdbd720..a746fe0 100644 --- a/lua/clipring/config.lua +++ b/lua/clipring/config.lua @@ -16,6 +16,7 @@ local M = {} ---@field preview_max_width number max preview width (`0` = content width up to screen edge) ---@field picker_max_height number max height of list and preview panes (lines) ---@field preview_max_lines number max lines shown in the preview pane for one entry +---@field preview_syntax boolean highlight preview when a code filetype is detected M.defaults = { max_entries = 100, @@ -33,6 +34,7 @@ M.defaults = { preview_max_width = 0, picker_max_height = 18, preview_max_lines = 16, + preview_syntax = true, } ---@type ClipRingConfig diff --git a/lua/clipring/preview_syntax.lua b/lua/clipring/preview_syntax.lua new file mode 100644 index 0000000..938a92c --- /dev/null +++ b/lua/clipring/preview_syntax.lua @@ -0,0 +1,228 @@ +local M = {} + +local DEFAULT_FT = "clipring_preview" + +local SKIP_FILETYPES = { + [""] = true, + text = true, + plaintext = true, + plaintex = true, +} + +--- Markdown fence language id -> file extension for vim.filetype.match. +local LANG_EXT = { + bash = "sh", + c = "c", + cpp = "cpp", + csharp = "cs", + cs = "cs", + docker = "dockerfile", + dockerfile = "dockerfile", + elixir = "ex", + ex = "ex", + go = "go", + golang = "go", + html = "html", + java = "java", + javascript = "js", + js = "js", + json = "json", + kotlin = "kt", + kt = "kt", + lua = "lua", + markdown = "md", + md = "md", + perl = "pl", + php = "php", + python = "py", + py = "py", + r = "r", + ruby = "rb", + rb = "rb", + rust = "rs", + rs = "rs", + scala = "scala", + sh = "sh", + shell = "sh", + sql = "sql", + swift = "swift", + toml = "toml", + typescript = "ts", + ts = "ts", + vim = "vim", + yaml = "yaml", + yml = "yaml", + zsh = "sh", +} + +local FENCE_OPEN = "^```(%S*)%s*$" +local FENCE_CLOSE = "^```%s*$" + +local function usable_filetype(ft) + return ft and ft ~= "" and not SKIP_FILETYPES[ft] +end + +---@param lang string +---@param contents string +---@return string|nil +local function filetype_from_lang(lang, contents) + lang = lang:lower() + local candidates = { LANG_EXT[lang], lang } + local seen = {} + for _, ext in ipairs(candidates) do + if ext and not seen[ext] then + seen[ext] = true + local ft = vim.filetype.match({ filename = "clipring." .. ext, contents = contents }) + if usable_filetype(ft) then + return ft + end + end + end + return nil +end + +---@param contents string +---@return string|nil +local function filetype_from_contents(contents) + local ft = vim.filetype.match({ contents = contents }) + if usable_filetype(ft) then + return ft + end + return nil +end + +---@param line string|nil +---@return string|nil +local function filetype_from_shebang(line) + if not line or not line:find("^#!") then + return nil + end + local lang = line:match("^#!%s*/usr/bin/env%s+(%S+)") + or line:match("^#!%s*/usr/bin/%s*(%S+)") + or line:match("^#!%s*/%S*/(%S+)%s*$") + if not lang then + return nil + end + lang = lang:gsub("^%-%S+", ""):match("^(%S+)") + if not lang then + return nil + end + return filetype_from_lang(lang, line) +end + +--- Heuristic content sniffing when there is no fence or shebang. +---@param lines string[] +---@param contents string +---@return string|nil +local function filetype_from_heuristics(lines, contents) + if #lines < 2 then + return nil + end + + local rules = { + { ft = "python", re = "\ndef %w+%([^)]*%)%s*:" }, + { ft = "python", re = "^def %w+%([^)]*%)%s*:" }, + { ft = "python", re = "\nfrom %w+ import " }, + { ft = "python", re = "^from %w+ import " }, + { ft = "python", re = "\nimport %w+" }, + { ft = "python", re = "^import %w+" }, + { ft = "ruby", re = "^class %w" }, + { ft = "ruby", re = "^module %w" }, + { ft = "ruby", re = "\n%s+def %w" }, + { ft = "ruby", re = "\ndef %w+%([^)]*%)%s*$" }, + { ft = "ruby", re = "\ndef %w+%([^)]*%)%s*\n" }, + { ft = "ruby", re = "^def %w+%([^)]*%)%s*$" }, + { ft = "javascript", re = "function%s+%w+%s*%(" }, + { ft = "javascript", re = "const%s+%w+%s*=" }, + { ft = "javascript", re = "=>%s*[%({]" }, + { ft = "typescript", re = "interface%s+%w+" }, + { ft = "lua", re = "\nlocal %w+" }, + { ft = "lua", re = "^local %w+" }, + { ft = "lua", re = "\nfunction%s+%w+%s*%(" }, + { ft = "lua", re = "^function%s+%w+%s*%(" }, + { ft = "go", re = "^package %w" }, + { ft = "go", re = "\nfunc %w" }, + { ft = "rust", re = "\nfn %w" }, + { ft = "rust", re = "^fn %w" }, + { ft = "rust", re = "\nuse %w" }, + { ft = "sql", re = "^%s*SELECT%s+" }, + { ft = "sql", re = "^%s*INSERT%s+INTO" }, + { ft = "sql", re = "^%s*CREATE%s+TABLE" }, + { ft = "html", re = "^]" }, + { ft = "json", re = "^%s*[%[{]" }, + { ft = "yaml", re = "^%s*[%w_%-]+%s*:" }, + { ft = "vim", re = "^%s*function%!" }, + { ft = "vim", re = "^%s*autocmd%s" }, + } + + for _, rule in ipairs(rules) do + if contents:find(rule.re) then + return rule.ft + end + end + + return nil +end + +---@param lines string[] +---@return string[] body, string|nil lang +function M.strip_fenced_codeblock(lines) + if #lines == 0 then + return lines, nil + end + + local lang = lines[1]:match(FENCE_OPEN) + if not lang then + return lines, nil + end + + lang = lang ~= "" and lang or nil + local body = {} + local last = #lines + if last > 1 and lines[last]:match(FENCE_CLOSE) then + last = last - 1 + end + for i = 2, last do + body[#body + 1] = lines[i] + end + return body, lang +end + +---@param lines string[] +---@param opts ClipRingConfig|nil +---@return string[] content_lines, string filetype +function M.analyze(lines, opts) + opts = opts or {} + if opts.preview_syntax == false then + return vim.deepcopy(lines), DEFAULT_FT + end + + local body, fence_lang = M.strip_fenced_codeblock(lines) + local contents = table.concat(body, "\n") + local ft + + if fence_lang then + ft = filetype_from_lang(fence_lang, contents) + end + + if not ft then + ft = filetype_from_shebang(body[1]) + end + + if not ft then + ft = filetype_from_contents(contents) + end + + if not ft then + ft = filetype_from_heuristics(body, contents) + end + + if not ft then + ft = DEFAULT_FT + end + + return body, ft +end + +return M diff --git a/lua/clipring/ui.lua b/lua/clipring/ui.lua index dbc0482..d4eb4ad 100644 --- a/lua/clipring/ui.lua +++ b/lua/clipring/ui.lua @@ -2,6 +2,7 @@ local config = require("clipring.config") local ring = require("clipring.ring") local paste = require("clipring.paste") local persist = require("clipring.persist") +local preview_syntax = require("clipring.preview_syntax") local M = {} @@ -110,29 +111,63 @@ local function refresh_list_buffer() end end -local function preview_lines_for_entry(entry) +---@param entry ClipRingEntry|nil +---@return string[] lines, string filetype +local function preview_content_for_entry(entry) if not entry or not entry.lines or #entry.lines == 0 then - return { "(empty)" } + return { "(empty)" }, "clipring_preview" end local opts = config.get() - local lines = vim.deepcopy(entry.lines) + local content_lines, filetype = preview_syntax.analyze(entry.lines, opts) local max_lines = opts.preview_max_lines - if max_lines > 0 and #lines > max_lines then + if max_lines > 0 and #content_lines > max_lines then local truncated = {} for i = 1, max_lines do - truncated[i] = lines[i] + truncated[i] = content_lines[i] end - table.insert(truncated, string.format("… (%d more lines)", #lines - max_lines)) - lines = truncated + table.insert(truncated, string.format("… (%d more lines)", #content_lines - max_lines)) + content_lines = truncated end + return content_lines, filetype +end + +local function preview_lines_for_entry(entry) + local content_lines = select(1, preview_content_for_entry(entry)) local padded = {} - for i, line in ipairs(lines) do + for i, line in ipairs(content_lines) do padded[i] = " " .. line end return padded end +local function apply_preview_filetype(filetype) + if not state.preview_buf or not vim.api.nvim_buf_is_valid(state.preview_buf) then + return + end + + local buf = state.preview_buf + vim.api.nvim_buf_set_option(buf, "modifiable", true) + vim.api.nvim_buf_set_option(buf, "filetype", filetype) + if filetype == "clipring_preview" then + vim.api.nvim_buf_set_option(buf, "syntax", "off") + if vim.treesitter and vim.treesitter.stop then + pcall(vim.treesitter.stop, buf) + end + else + vim.api.nvim_buf_set_option(buf, "syntax", "on") + if vim.treesitter and vim.treesitter.language and vim.treesitter.start then + pcall(function() + local lang = vim.treesitter.language.get_lang(filetype) + if lang then + vim.treesitter.start(buf, lang) + end + end) + end + end + vim.api.nvim_buf_set_option(buf, "modifiable", false) +end + local function entry_has_preview_content(entry) if not entry or not entry.lines or #entry.lines == 0 then return false @@ -156,7 +191,13 @@ local function refresh_preview_buffer() end local entry = all[state.index] - set_buf_lines(state.preview_buf, preview_lines_for_entry(entry)) + local content_lines, filetype = preview_content_for_entry(entry) + local padded = {} + for i, line in ipairs(content_lines) do + padded[i] = " " .. line + end + set_buf_lines(state.preview_buf, padded) + apply_preview_filetype(filetype) end local function max_preview_line_width(lines) diff --git a/tests/preview_syntax_spec.lua b/tests/preview_syntax_spec.lua new file mode 100644 index 0000000..2482b6d --- /dev/null +++ b/tests/preview_syntax_spec.lua @@ -0,0 +1,78 @@ +local preview_syntax = require("clipring.preview_syntax") + +describe("clipring.preview_syntax", function() + it("strips markdown fenced code blocks and detects the fence language", function() + local body, lang = preview_syntax.strip_fenced_codeblock({ + "```ruby", + "def foo", + "end", + "```", + }) + assert.are.equal("ruby", lang) + assert.same({ "def foo", "end" }, body) + end) + + it("keeps plain lines when there is no fence", function() + local body, lang = preview_syntax.strip_fenced_codeblock({ "hello", "world" }) + assert.is_nil(lang) + assert.same({ "hello", "world" }, body) + end) + + it("detects ruby from a fenced block", function() + local _, ft = preview_syntax.analyze({ + "```ruby", + "def foo", + " 1", + "end", + "```", + }) + assert.are.equal("ruby", ft) + end) + + it("detects lua from a fenced block", function() + local _, ft = preview_syntax.analyze({ + "```lua", + "local function foo()", + " return 1", + "end", + "```", + }) + assert.are.equal("lua", ft) + end) + + it("heuristically detects ruby without a fence", function() + local _, ft = preview_syntax.analyze({ + "class Widget", + " def name", + " 'clipring'", + " end", + "end", + }) + assert.are.equal("ruby", ft) + end) + + it("heuristically detects python without a fence", function() + local _, ft = preview_syntax.analyze({ + "import os", + "def main():", + " print('hi')", + }) + assert.are.equal("python", ft) + end) + + it("falls back to clipring_preview for plain prose", function() + local lines, ft = preview_syntax.analyze({ "Just a short note." }) + assert.same({ "Just a short note." }, lines) + assert.are.equal("clipring_preview", ft) + end) + + it("can be disabled via config", function() + local _, ft = preview_syntax.analyze({ + "```ruby", + "def foo", + "end", + "```", + }, { preview_syntax = false }) + assert.are.equal("clipring_preview", ft) + end) +end) diff --git a/tests/ui_spec.lua b/tests/ui_spec.lua index 7c1d9d7..20f2c82 100644 --- a/tests/ui_spec.lua +++ b/tests/ui_spec.lua @@ -97,6 +97,22 @@ describe("clipring.ui", function() ui.close() end) + it("sets ruby filetype for fenced code blocks in the preview pane", function() + ring.clear() + ring.add({ + "```ruby", + "def foo", + " 1", + "end", + "```", + }, "V") + ui.open() + local preview_buf = h.find_clipring_preview_buf() + assert.are.equal("ruby", vim.api.nvim_buf_get_option(preview_buf, "filetype")) + assert.same({ " def foo", " 1", " end" }, vim.api.nvim_buf_get_lines(preview_buf, 0, -1, false)) + ui.close() + end) + it("shows multiline yank content in the preview pane", function() ring.clear() ring.add({ "alpha", "beta", "gamma" }, "V")