nvim-deck : A plugin for displaying, filtering, and selecting items from customizable lists.
Open project files
CleanShot.2024-12-08.at.11.24.57.mp4
Grep and Replace
CleanShot.2024-12-08.at.11.45.57.mp4
Git integration
CleanShot.2024-12-08.at.12.01.21.mp4
nvim-deck revolves around four core concepts:
- Source:
- A source provides a list of items to display.
- Item:
- An item represents a single entry from the source, containing data and display text.
- Action:
- Actions define what happens when the user interacts with an item.
- Context:
- Context represents the current state, The user can control deck UI via invoke context methods.
- Built-in Git integration.
:Deck git
to open the git launcher.
- Built-in ripgrep integration.
:Deck grep
to start a grep search.
- Built-in file listing.
:Deck files
to show files under the root directory (ripgrep
,PureLua
).
- Highly customizable: sources, actions, previewers, decorators, views, and matchers.
- Use normal-window by default
- IMO, floating-window is fancy but normal window is more handy for edit & preview.
- UX focused
- Use
vim.wait
carefully, it makes a crisp and smooth experience.
- Use
- Configuration over Object control
nvim-deck
does not require huge setup functions- Instead of this, nvim-deck allows you to customize by controlling |deck.Context| object.
Here’s an example of how to set up nvim-deck
:
local deck = require('deck')
-- Apply pre-defined easy settings.
-- For manual configuration, refer to the code in `deck/easy.lua`.
require('deck.easy').setup()
-- Set up buffer-specific key mappings for nvim-deck.
vim.api.nvim_create_autocmd('User', {
pattern = 'DeckStart',
callback = function(e)
local ctx = e.data.ctx --[[@as deck.Context]]
-- normal-mode mapping.
ctx.keymap('n', '<Esc>', function()
ctx.set_preview_mode(false)
end)
ctx.keymap('n', '<Tab>', deck.action_mapping('choose_action'))
ctx.keymap('n', '<C-l>', deck.action_mapping('refresh'))
ctx.keymap('n', 'i', deck.action_mapping('prompt'))
ctx.keymap('n', 'a', deck.action_mapping('prompt'))
ctx.keymap('n', '@', deck.action_mapping('toggle_select'))
ctx.keymap('n', '*', deck.action_mapping('toggle_select_all'))
ctx.keymap('n', 'p', deck.action_mapping('toggle_preview_mode'))
ctx.keymap('n', 'd', deck.action_mapping('delete'))
ctx.keymap('n', '<CR>', deck.action_mapping('default'))
ctx.keymap('n', 'o', deck.action_mapping('open'))
ctx.keymap('n', 'O', deck.action_mapping('open_keep'))
ctx.keymap('n', 's', deck.action_mapping('open_split'))
ctx.keymap('n', 'v', deck.action_mapping('open_vsplit'))
ctx.keymap('n', 'N', deck.action_mapping('create'))
ctx.keymap('n', '<C-u>', deck.action_mapping('scroll_preview_up'))
ctx.keymap('n', '<C-d>', deck.action_mapping('scroll_preview_down'))
-- cmdline-mode mapping.
ctx.keymap('c', '<C-y>', function()
vim.api.nvim_feedkeys(vim.keycode('<Esc>'), 'n', true)
vim.schedule(function()
ctx.do_action('default')
end)
end)
ctx.keymap('c', '<C-j>', function()
ctx.set_cursor(ctx.get_cursor() + 1)
end)
ctx.keymap('c', '<C-k>', function()
ctx.set_cursor(ctx.get_cursor() - 1)
end)
-- If you want to start the filter by default, call ctx.prompt() here
ctx.prompt()
end
})
-- Example key bindings for launching nvim-deck sources. (These mapping required `deck.easy` calls.)
vim.keymap.set('n', '<Leader>ff', '<Cmd>Deck files<CR>', { desc = 'Show recent files, buffers, and more' })
vim.keymap.set('n', '<Leader>gr', '<Cmd>Deck grep<CR>', { desc = 'Start grep search' })
vim.keymap.set('n', '<Leader>gi', '<Cmd>Deck git<CR>', { desc = 'Open git launcher' })
vim.keymap.set('n', '<Leader>he', '<Cmd>Deck helpgrep<CR>', { desc = 'Live grep all help tags' })
-- Show the latest deck context.
vim.keymap.set('n', '<Leader>;', function()
local ctx = require('deck').get_history()[1]
if ctx then
ctx.show()
end
end)
-- Do default action on next item.
vim.keymap.set('n', '<Leader>n', function()
local ctx = require('deck').get_history()[1]
if ctx then
ctx.set_cursor(ctx.get_cursor() + 1)
ctx.do_action('default')
end
end)
!!! We strongly recommend using lua-language-server
!!!
You can customize your nvim-deck appearance.
- guicursor
require('deck').setup({
guicursor = 'a:ver25',
})
- winhighlight
vim.api.nvim_create_autocmd('User', {
group = misc.group,
pattern = 'DeckShow',
callback = function()
vim.wo.winhighlight = 'CursorLine:Visual'
end
})
The source is typed as |deck.Source|. The source can be executed by
deck.start()
.
The source can specify source-level actions, decorators, and previewers.
require('deck').start({
name = 'my_source',
execute = function(ctx)
ctx.item({
display_text = 'Hello, World!',
})
ctx.done()
end,
actions = {
...
},
decorators = {
...
},
previewers = {
...
},
})
The action is typed as |deck.Action| and can be registered below three different levels.
- Config Action : Provided by a start configuration.
This level can be registered by start_config.actions
field.
require('deck').start(some_of_the_source, {
actions = {
{
name = 'my_open',
execute = function(ctx)
-- some of the process.
end
}
}
})
- Source Action : Provided by a source.
This level of actions can be registered by source.actions
field.
require('deck').start({
name = 'my_source',
...
actions = {
{
name = 'my_open',
execute = function(ctx)
-- some of the process.
end
}
}
})
- Global Action : Registered globally.
This level of actions can be registered by deck.register_action()
.
require('deck').register_action({
name = 'my_open',
resolve = function(ctx)
-- Action is available only if there is exactly one action item with a filename.
return #ctx.get_action_items() == 1 and ctx.get_action_items()[1].data.filename
end,
execute = function(ctx)
-- Open the file.
vim.cmd.edit(ctx.get_action_items()[1].data.filename)
end
})
Note: The same name actions are choosen in the order of 1 -> 2 -> 3.
The start-preset is typed as |deck.StartPreset| and can be registered globally.
A start-preset in nvim-deck
allows you to define shortcut command. In the
below example, you can use :Deck recent
command.
-- After registration, you can start the preset using the `:Deck recent` command.
require('deck').register_start_preset('recent', {
require('deck').start({
require('deck.builtin.source.recent_files')(),
require('deck.builtin.source.buffers')(),
})
})
nvim-deck
has decorator
concept. It's designed to decorate the deck-buffer
via nvim_buf_set_extmark
. The below example shows how to create your own
decorator.
--- This is example decorator.
--- To display the basename of the file and dirname as a comment.
--- This decorator highlight basename and make dirname less noticeable.
require('deck').register_decorator({
name = 'basename_dirname',
resolve = function(_, item)
-- This decorator is available only if the item has a filename.
return item.data.filename
end,
decorate = function(ctx, item, row)
local dirname = vim.fn.fnamemodify(item.data.filename, ':~:h')
local display_text = item.display_text
local s, e = display_text:find(dirname, 1, true)
if s then
return {
-- Hide the directory part (using conceal)
{
row = row,
col = s - 1,
end_row = row,
end_col = e + 1,
conceal = '',
ephemeral = true,
},
-- Display the directory name as a comment at the end of the line
{
row = row,
col = 0,
virt_text = { { dirname, 'Comment' } },
virt_text_pos = 'eol'
}
}
end
return {}
end
})
nvim-deck
has previewer
concept. It's designed to show the item preview.
require('deck').register_previewer({
name = 'bat',
resolve = function(_, item)
return item.data.filename and vim.fn.filereadable(item.data.filename) == 1
end,
preview = function(_, item, env)
vim.api.nvim_win_call(env.win, function()
vim.fn.termopen(('bat --color=always %s'):format(item.data.filename))
end)
end
})
Show buffers.
Name | Type | Default | Description |
---|---|---|---|
ignore_paths | string[]? | [vim.fn.expand('%:p')] | Ignore paths. The default value is intented to hide current buffer. |
nofile | boolean? | false | Ignore nofile buffers. |
deck.start(require('deck.builtin.source.buffers')({
ignore_paths = { vim.fn.expand('%:p'):gsub('/$', '') },
nofile = false,
}))
Show available actions from |deck.Context|
Name | Type | Default | Description |
---|---|---|---|
context | |deck.Context| |
deck.start(require('deck.builtin.source.deck.actions')({
context = context
}))
Show deck.start history.
No options
deck.start(require('deck.builtin.source.deck.history')())
Show files under specified root directory.
Name | Type | Default | Description |
---|---|---|---|
ignore_globs | string[]? | [] | Ignore glob patterns. |
root_dir | string | Target root directory. |
deck.start(require('deck.builtin.source.files')({
root_dir = vim.fn.getcwd(),
ignore_globs = { '**/node_modules/', '**/.git/' },
}))
Show git launcher.
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. |
deck.start(require('deck.builtin.source.git.changeset')({
cwd = vim.fn.getcwd(),
}))
Show git branches
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. |
deck.start(require('deck.builtin.source.git.branch')({
cwd = vim.fn.getcwd()
}))
Show git changeset for specified revision.
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. | |
from_rev | string | From revision. | |
to_rev | string? | To revision. If you omit this option, it will be HEAD. |
deck.start(require('deck.builtin.source.git.changeset')({
cwd = vim.fn.getcwd(),
from_rev = 'HEAD~3',
to_rev = 'HEAD'
}))
Show git log.
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. | |
max_count | integer? | Max count for log |
deck.start(require('deck.builtin.source.git.log')({
cwd = vim.fn.getcwd(),
}))
Show git reflog.
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. | |
max_count | integer? | Max count for reflog |
deck.start(require('deck.builtin.source.git.reflog')({
cwd = vim.fn.getcwd(),
}))
Show git remotes.
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. |
deck.start(require('deck.builtin.source.git.remote')({
cwd = vim.fn.getcwd(),
}))
Show git status.
Name | Type | Default | Description |
---|---|---|---|
cwd | string | Target git root. |
deck.start(require('deck.builtin.source.git.status')({
cwd = vim.fn.getcwd(),
}))
Grep files under specified root directory. (required ripgrep
)
Name | Type | Default | Description |
---|---|---|---|
root_dir | string | Target root directory. | |
pattern | string? | Grep pattern. If you omit this option, you must set dynamic option to true. |
|
dynamic | boolean? | false | If true, use dynamic pattern. If you set this option to false, you must set pattern option. |
ignore_globs | string[]? | [] | Ignore glob patterns. |
deck.start(require('deck.builtin.source.grep')({
root_dir = vim.fn.getcwd(),
pattern = vim.fn.input('grep: '),
ignore_globs = { '**/node_modules/', '**/.git/' },
}))
Live grep all helptags. (required ripgrep
)
No options
deck.start(require('deck.builtin.source.helpgrep')())
Listing any provided items.
Name | Type | Default | Description |
---|---|---|---|
items | string[]|deck.ItemSpecifier[] | Items to list. |
deck.start(require('deck.builtin.source.items')({
items = vim.iter(vim.api.nvim_list_bufs()):map(function(buf)
return ('#%s'):format(buf)
end):totable()
}))
Show buffer lines.
No options
deck.start(require('deck.builtin.source.lines')({
bufnrs = { vim.api.nvim_get_current_buf() },
}))
List recent directories.
Name | Type | Default | Description |
---|---|---|---|
ignore_paths | string[]? | [] | Ignore paths. |
require('deck.builtin.source.recent_dirs'):setup({
path = '~/.deck.recent_dirs'
})
vim.api.nvim_create_autocmd('DirChanged', {
callback = function(e)
require('deck.builtin.source.recent_dirs'):add(e.cwd)
end,
})
deck.start(require('deck.builtin.source.recent_dirs')({
ignore_paths = { '**/node_modules/', '**/.git/' },
}))
List recent files.
Name | Type | Default | Description |
---|---|---|---|
ignore_paths | string[]? | [] | Ignore paths. |
require('deck.builtin.source.recent_files'):setup({
path = '~/.deck.recent_files'
})
vim.api.nvim_create_autocmd('BufEnter', {
callback = function()
local bufname = vim.api.nvim_buf_get_name(0)
if vim.fn.filereadable(bufname) == 1 then
require('deck.builtin.source.recent_files'):add(vim.fs.normalize(bufname))
end
end,
})
deck.start(require('deck.builtin.source.recent_files')({
ignore_paths = { '**/node_modules/', '**/.git/' },
}))
-
choose_action
-
Open action source.
The actions listed are filtered by whether they are valid in the current context.
-
-
delete_buffer
-
Delete
item.data.bufnr
from buffers list.If multiple items are selected, they will be deleted in order.
-
-
delete_file
-
Delete
item.data.filename
from filesystem.If multiple items are selected, they will be deleted in order.
-
-
open
-
Open
item.data.filename
oritem.data.bufnr
.Open at the recently normal window.
-
-
open_keep
-
Open
item.data.filename
oritem.data.bufnr
.But keep the deck window and cursor.
-
-
open_split
-
Open
item.data.filename
oritem.data.bufnr
.Open at the recently normal window with split.
-
-
open_vsplit
-
Open
item.data.filename
oritem.data.bufnr
.Open at the recently normal window with vsplit.
-
-
print
- Print selected items.
-
prompt
- Open filtering prompt
-
refresh
- Re-execute source. (it can be used to refresh the items)
-
scroll_preview_down
- Scroll preview window down.
-
scroll_preview_up
- Scroll preview window up.
-
substitute
-
Open substitute buffer with selected items (
item.data.filename
anditem.data.lnum
are required).You can modify and save the buffer to reflect the changes to the original files.
-
-
toggle_preview_mode
- Toggle preview mode
-
toggle_select
- Toggle selected state of the cursor item.
-
toggle_select_all
- Toggle selected state of all items.
-
yank
- Yank item.display_text field to default register.
-
DeckHide
- Triggered after deck window hidden.
-
DeckShow
- Triggered after deck window shown.
-
DeckStart
- Triggered when deck starts.
-
DeckStart:{source_name}
- Triggered when deck starts for source.
Create action mapping function for ctx.keymap.
Name | Type | Description |
---|---|---|
action_names | string|string[] | action name or action names to use for mappings. |
Create alias action.
Name | Type | Description |
---|---|---|
alias_name | string | new action name. |
alias_action_name | string | existing action name. |
Get all registered actions.
No arguments
Get all registered decorators.
No arguments
Get all history (first history is latest).
No arguments
Get all registered previewers.
No arguments
Get all registered start presets.
No arguments
Register action.
Name | Type | Description |
---|---|---|
action | |deck.Action| | action to register. |
Register decorator.
Name | Type | Description |
---|---|---|
decorator | |deck.Decorator| | decorator to register. |
Register previewer.
Name | Type | Description |
---|---|---|
previewer | |deck.Previewer| | previewer to register. |
Register start_preset.
Name | Type | Description |
---|---|---|
name | string | preset name. |
start_fn | fun() | Start function. |
Register start_preset.
Name | Type | Description |
---|---|---|
start_preset | deck.StartPreset | |deck.StartPreset| |
Remove specific action.
Name | Type | Description |
---|---|---|
predicate | fun(action: |deck.Action|): boolean | Predicate function. If return true, remove action. |
Remove specific decorator.
Name | Type | Description |
---|---|---|
predicate | fun(decorator: |deck.Decorator|): boolean | Predicate function. If return true, remove decorator. |
Remove previewer.
Name | Type | Description |
---|---|---|
predicate | fun(previewer: |deck.Previewer|): boolean | Predicate function. If return true, remove previewer. |
Remove specific start_preset.
Name | Type | Description |
---|---|---|
predicate | fun(start_preset: |deck.StartPreset|): boolean | Predicate function. If return true, remove start_preset. |
Setup deck globally.
Name | Type | Description |
---|---|---|
config | deck.ConfigSpecifier | Setup deck configuration. |
Start deck with given sources.
Name | Type | Description |
---|---|---|
source | deck.Source|deck.Source[] | source or sources to start. |
start_config | deck.StartConfigSpecifier | start configuration. |
*deck.Action*
---@class deck.Action
---@field public name string
---@field public desc? string
---@field public hidden? boolean
---@field public resolve? deck.ActionResolveFunction
---@field public execute deck.ActionExecuteFunction
*deck.Config*
---@class deck.Config: deck.ConfigSpecifier
---@field public guicursor? string
---@field public max_history_size integer
---@field public default_start_config? deck.StartConfigSpecifier
*deck.Context*
---@class deck.Context
---@field id integer
---@field ns integer
---@field buf integer
---@field name string
---@field execute fun()
---@field is_visible fun(): boolean
---@field show fun()
---@field hide fun()
---@field prompt fun()
---@field scroll_preview fun(delta: integer)
---@field get_status fun(): deck.Context.Status
---@field get_cursor fun(): integer
---@field set_cursor fun(cursor: integer)
---@field get_query fun(): string
---@field set_query fun(query: string)
---@field set_selected fun(item: deck.Item, selected: boolean)
---@field get_selected fun(item: deck.Item): boolean
---@field set_select_all fun(select_all: boolean)
---@field get_select_all fun(): boolean
---@field set_preview_mode fun(preview_mode: boolean)
---@field get_preview_mode fun(): boolean
---@field get_items fun(): deck.Item[]
---@field get_cursor_item fun(): deck.Item?
---@field get_action_items fun(): deck.Item[]
---@field get_filtered_items fun(): deck.Item[]
---@field get_selected_items fun(): deck.Item[]
---@field get_actions fun(): deck.Action[]
---@field get_decorators fun(): deck.Decorator[]
---@field get_previewer fun(): deck.Previewer?
---@field get_revision fun(): deck.Context.Revision
---@field get_source_names fun(): string[]
---@field sync fun(option: { count: integer })
---@field keymap fun(mode: string, lhs: string, rhs: fun(ctx: deck.Context))
---@field do_action fun(name: string)
---@field dispose fun()
---@field disposed fun(): boolean
---@field on_show fun(callback: fun())
---@field on_hide fun(callback: fun())
---@field on_dispose fun(callback: fun()): fun()
*deck.Decoration*
---@class deck.Decoration
---@field public col? integer
---@field public end_col? integer
---@field public hl_group? string
---@field public virt_text? deck.VirtualText[]
---@field public virt_text_pos? 'eol' | 'overlay' | 'right_align' | 'inline'
---@field public virt_text_win_col? integer
---@field public virt_text_hide? boolean
---@field public virt_text_repeat_linebreak? boolean
---@field public virt_lines? deck.VirtualText[][]
---@field public virt_lines_above? boolean
---@field public ephemeral? boolean
---@field public priority? integer
---@field public sign_text? string
---@field public sign_hl_group? string
---@field public number_hl_group? string
---@field public line_hl_group? string
---@field public conceal? boolean
*deck.Decorator*
---@class deck.Decorator
---@field public name string
---@field public dynamic? boolean
---@field public resolve? deck.DecoratorResolveFunction
---@field public decorate deck.DecoratorDecorateFunction
*deck.ExecuteContext*
---@class deck.ExecuteContext
---@field public item fun(item: deck.ItemSpecifier)
---@field public done fun( )
---@field public get_query fun(): string
---@field public aborted fun(): boolean
---@field public on_abort fun(callback: fun())
*deck.Item*
---@class deck.Item: deck.ItemSpecifier
---@field public display_text string
---@field public data table
*deck.Previewer*
---@class deck.Previewer
---@field public name string
---@field public resolve? deck.PreviewerResolveFunction
---@field public preview deck.PreviewerPreviewFunction
*deck.Source*
---@class deck.Source
---@field public name string
---@field public dynamic? boolean
---@field public events? { Start?: fun(ctx: deck.Context), BufWinEnter?: fun(ctx: deck.Context, env: { first: boolean }) }
---@field public execute deck.SourceExecuteFunction
---@field public actions? deck.Action[]
---@field public decorators? deck.Decorator[]
---@field public previewers? deck.Previewer[]
*deck.StartConfig*
---@class deck.StartConfig: deck.StartConfigSpecifier
---@field public name string
---@field public view fun(): deck.View
---@field public matcher deck.Matcher
---@field public history boolean
---@field public performance { interrupt_interval: integer, interrupt_timeout: integer }
---@field public dedup boolean
---@field public parse_query fun(query: string, source: deck.Source): { filter: string, dynamic: string }
*deck.StartPreset*
---@class deck.StartPreset
---@field public name string
---@field public args? table<string|integer, { complete?: (fun(prefix: string):string[]), required?: boolean }>
---@field public start fun(args: table<string|integer, string>)
*deck.View*
---@class deck.View
---@field public get_win fun(): integer?
---@field public is_visible fun(ctx: deck.Context): boolean
---@field public show fun(ctx: deck.Context)
---@field public hide fun(ctx: deck.Context)
---@field public prompt fun(ctx: deck.Context)
---@field public scroll_preview fun(ctx: deck.Context, delta: integer)
---@field public render fun(ctx: deck.Context)