Skip to content

nvim-deck: A plugin to display, filter, and choose from lists of items.

Notifications You must be signed in to change notification settings

hrsh7th/nvim-deck

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Intro

nvim-deck : A plugin for displaying, filtering, and selecting items from customizable lists.

Demo

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

Concept

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.

Features

  • 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.

Why nvim-deck?

  • 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.
  • 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.

Setup

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)

Customization

!!! We strongly recommend using lua-language-server !!!

Appearance

You can customize your nvim-deck appearance.

  1. guicursor
require('deck').setup({
  guicursor = 'a:ver25',
})
  1. winhighlight
vim.api.nvim_create_autocmd('User', {
  group = misc.group,
  pattern = 'DeckShow',
  callback = function()
    vim.wo.winhighlight = 'CursorLine:Visual'
  end
})

Source

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 = {
    ...
  },
})

Action

The action is typed as |deck.Action| and can be registered below three different levels.

  1. 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
    }
  }
})
  1. 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
    }
  }
})
  1. 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.

StartPreset

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')(),
  })
})

Decorator

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
})

Previewer

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
})

Built-in

Sources

buffers

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,
}))

deck.actions

Show available actions from |deck.Context|

Name Type Default Description
context |deck.Context|
deck.start(require('deck.builtin.source.deck.actions')({
  context = context
}))

deck.history

Show deck.start history.

No options

deck.start(require('deck.builtin.source.deck.history')())

files

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/' },
}))

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(),
}))

git.branch

Show git branches

Name Type Default Description
cwd string Target git root.
deck.start(require('deck.builtin.source.git.branch')({
  cwd = vim.fn.getcwd()
}))

git.changeset

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'
}))

git.log

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(),
}))

git.reflog

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(),
}))

git.remote

Show git remotes.

Name Type Default Description
cwd string Target git root.
deck.start(require('deck.builtin.source.git.remote')({
  cwd = vim.fn.getcwd(),
}))

git.status

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

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/' },
}))

helpgrep

Live grep all helptags. (required ripgrep)

No options

deck.start(require('deck.builtin.source.helpgrep')())

items

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()
}))

lines

Show buffer lines.

No options

deck.start(require('deck.builtin.source.lines')({
  bufnrs = { vim.api.nvim_get_current_buf() },
}))

recent_dirs

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/' },
}))

recent_files

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/' },
}))

Actions

  • 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 or item.data.bufnr.

      Open at the recently normal window.

  • open_keep

    • Open item.data.filename or item.data.bufnr.

      But keep the deck window and cursor.

  • open_split

    • Open item.data.filename or item.data.bufnr.

      Open at the recently normal window with split.

  • open_vsplit

    • Open item.data.filename or item.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 and item.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.

Autocmd

  • 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.

API

deck.action_mapping(mapping): fun(ctx: |deck.Context|)

Create action mapping function for ctx.keymap.

Name Type Description
action_names string|string[] action name or action names to use for mappings.

 

deck.alias_action(alias_name, alias_action_name): |deck.Action|

Create alias action.

Name Type Description
alias_name string new action name.
alias_action_name string existing action name.

 

deck.get_actions(): |deck.Action|[]

Get all registered actions.

No arguments  

deck.get_decorators(): |deck.Decorator|[]

Get all registered decorators.

No arguments  

deck.get_history(): |deck.Context|[]

Get all history (first history is latest).

No arguments  

deck.get_previewers(): |deck.Previewer|[]

Get all registered previewers.

No arguments  

deck.get_start_presets(): |deck.StartPreset|[]

Get all registered start presets.

No arguments  

deck.register_action(action)

Register action.

Name Type Description
action |deck.Action| action to register.

 

deck.register_decorator(decorator)

Register decorator.

Name Type Description
decorator |deck.Decorator| decorator to register.

 

deck.register_previewer(previewer)

Register previewer.

Name Type Description
previewer |deck.Previewer| previewer to register.

 

deck.register_start_preset(name, start_fn)

Register start_preset.

Name Type Description
name string preset name.
start_fn fun() Start function.

 

deck.register_start_preset(start_preset)

Register start_preset.

Name Type Description
start_preset deck.StartPreset |deck.StartPreset|

 

deck.remove_actions(predicate)

Remove specific action.

Name Type Description
predicate fun(action: |deck.Action|): boolean Predicate function. If return true, remove action.

 

deck.remove_decorators(predicate)

Remove specific decorator.

Name Type Description
predicate fun(decorator: |deck.Decorator|): boolean Predicate function. If return true, remove decorator.

 

deck.remove_previewers(predicate)

Remove previewer.

Name Type Description
predicate fun(previewer: |deck.Previewer|): boolean Predicate function. If return true, remove previewer.

 

deck.remove_start_presets(predicate)

Remove specific start_preset.

Name Type Description
predicate fun(start_preset: |deck.StartPreset|): boolean Predicate function. If return true, remove start_preset.

 

deck.setup(config)

Setup deck globally.

Name Type Description
config deck.ConfigSpecifier Setup deck configuration.

 

deck.start(sources, start_config): |deck.Context|

Start deck with given sources.

Name Type Description
source deck.Source|deck.Source[] source or sources to start.
start_config deck.StartConfigSpecifier start configuration.

 

Type

*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)

About

nvim-deck: A plugin to display, filter, and choose from lists of items.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages