A Neovim plugin that provides in-process LSP server, a community library and a convenient interface for customization and enhancement of your Neovim with custom code actions.
- 🚀 In-process LSP server for code actions
- đź§© Simple, intuitive interface and helpers for managing and creating code actions
- 🔍 Enhanced code actions picker with live filtering, grouping, keymaps and extra info
- 📚 Community-driven library of useful code actions
With lazy.nvim, everything should work out of the box.
You may want to tweak a few options, notably global keymaps and perhaps choose which actions to include/exclude, to keep the picker clean and fast.
{
'yarospace/dev-tools.nvim',
dependencies = {
"nvim-treesitter/nvim-treesitter", -- code manipulation in buffer, required
{
"folke/snacks.nvim", -- optional
opts = {
picker = { enabled = true }, -- actions picker
terminal = { enabled = true }, -- terminal for running spec actions
},
},
{
"ThePrimeagen/refactoring.nvim", -- refactoring library, optional
dependencies = { "nvim-lua/plenary.nvim" },
},
},
opts = {
---@type Action[]|fun():Action[]
actions = {},
filetypes = { -- filetypes for which to attach the LSP
include = {}, -- {} to include all, except for special buftypes, e.g. nofile|help|terminal|prompt
exclude = {},
},
}
}
For other package managers, you may need to include dependencies and call require('dev-tools').setup({ ... })
in your config.
Minimal Config
{
'yarospace/dev-tools.nvim',
dependencies = {
"nvim-treesitter/nvim-treesitter", -- code manipulation in buffer, required
{
"folke/snacks.nvim", -- optional
opts = {
picker = { enabled = true }, -- actions picker
terminal = { enabled = true }, -- terminal for running spec actions
},
},
{
"ThePrimeagen/refactoring.nvim", -- refactoring library, optional
dependencies = { "nvim-lua/plenary.nvim" },
},
},
opts = {
---@type Action[]|fun():Action[]
actions = {},
filetypes = { -- filetypes for which to attach the LSP
include = {}, -- {} to include all, except for special buftypes, e.g. nofile|help|terminal|prompt
exclude = {},
},
builtin_actions = {
include = {}, -- filetype/group/name of actions to include or {} to include all
exclude = {}, -- filetype/group/name of actions to exclude or "true" to exclude all
},
action_opts = { -- override options for actions
{
group = "Debugging",
name = "Log vars under cursor",
opts = {
keymap = nil, ---@type Keymap action keymap spec, e.g.
-- {
-- global = "<leader>dl" | { "<leader>dl", mode = { "n", "x" } },
-- picker = "<M-l>",
-- hide = true, -- hide the action from the picker
-- }
},
},
},
ui = {
override = true, -- override vim.ui.select, requires `snacks.nvim` to be included in dependencies or installed separately
group_actions = true, -- group actions by group
},
}
}
Full Config
local M = {
---@type Action[]|fun():Action[]
actions = {},
filetypes = { -- filetypes for which to attach the LSP
include = {}, -- {} to include all, except for special buftypes, e.g. nofile|help|terminal|prompt
exclude = {},
},
builtin_actions = {
include = {}, -- filetype/group/name of actions to include or {} to include all
exclude = {}, -- filetype/group/name of actions to exclude or "true" to exclude all
},
action_opts = { -- override default options for actions
{
group = "Debugging",
name = "Log vars under cursor",
opts = {
logger = nil, ---@type function to log debug info, default dev-tools.log
keymap = nil, ---@type Keymap action keymap spec, e.g.
-- {
-- global = "<leader>dl"|{ "<leader>dl", mode = { "n", "x" } },
-- picker = "<M-l>",
-- hide = true, -- hide the action from the picker
-- }
},
},
{
group = "Specs",
name = "Watch specs",
opts = {
tree_cmd = nil, ---@type string command to run the file tree, default "git ls-files -cdmo --exclude-standard"
test_cmd = nil, ---@type string command to run tests, default "nvim -l tests/minit.lua tests --shuffle-tests -v"
test_tag = nil, ---@type string test tag, default "wip"
terminal_cmd = nil, ---@type function to run the terminal, default is Snacks.terminal
},
},
{
group = "Todo",
name = "Open Todo",
opts = {
filename = nil, ---@type string name of the todo file, default ".todo.md"
template = nil, ---@type string[] -- template for the todo file
},
},
},
ui = {
override = true, -- override vim.ui.select, requires `snacks.nvim` to be included in dependencies or installed separately
group_actions = true, -- group actions by group
keymaps = { filter = "<C-b>", open_group = "<C-l>", close_group = "<C-h>" },
},
debug = false, -- extra debug info
cache = true, -- cache the actions on start
}
Note
Dev-tools picker uses Snacks.nvim picker module, which should be included as a dependency or installed separately.
Some plugins, like lsp-saga
or nvim-lighbulb
show đź’ˇ signs when there are code actions available.
Since dev-tools provides code actions for every line, you may want to disable it in your config. For example:
require('lspsaga').setup({
lightbulb = { ignore = { clients = { 'dev-tools' } } }
})
require("nvim-lightbulb").setup({
ignore = { clients = { "dev-tools" } }
})
- Code actions are accessible via the default LSP keymaps, e.g.
gra
,<leader>ca
,<leader>la
, etc. - Last action is dot-repeatable.
- You can add a global or a picker local keymap by specifying it in the
keymap
table of theaction_opts
.
Dev-tools actions picker is an enhanced version of the default picker, which provides extra info about the actions, live filtering and actions keymaps.
<C-b>
will cycle through categories filter
- If
opts.ui.group_actions
is set totrue
, the actions will be grouped by group name.
Use<C-l>
to open the group and<C-h>
to close.
- Custom actions can be added to the
opts.actions
table in your configuration
or registered viarequire('dev-tools').register_action({})
---@class Action
---@field name string - name of the action
---@field group string|nil - group of the action
---@field condition string|nil|fun(action: ActionCtx): boolean - function or pattern to match against buffer name
---@field filetype string[]|nil - filetype to limit the action to
---@field fn fun(action: ActionCtx) - function to execute the action
---@class ActionCtx: Action
---@field ctx Ctx - context of the action
---@class Ctx
---@field buf number - buffer number
---@field win number - window number
---@field row number - current line number
---@field col number - current column number
---@field line string - current line
---@field word string - word under cursor
---@field ts_node TSNode|nil - current TS node
---@field ts_type string|nil - type of the current TS node
---@field ts_range table<number, number, number, number>|nil - range of the current TS node
---@field bufname string - full path to file in buffer
---@field root string - root directory of the file
---@field filetype string - filetype
---@field range Range|nil - range of the current selection
---@field edit Edit - edititng functions
---@class Range
---@field start {line: number, character: number} - start position of the range
---@field end {line: number, character: number} - end position of the range
---@field rc table<number, number, number, number> - row/col format
opts = {
---@type Action[]|fun():Action[]
actions = {
{
name = "Extract variable",
filetype = { "lua" },
fn = function(action)
local ctx = action.ctx
vim.ui.input({ prompt = "Variable name:", default = "" }, function(var_name)
if not var_name then return end
local var_body = ("local %s = %s"):format(var_name, ctx.edit:get_range()[1])
ctx.edit:set_range { var_name }
ctx.edit:set_lines({ var_body }, ctx.range.rc[1], ctx.range.rc[1])
ctx.edit:indent(ctx.range.rc[1] - 1, ctx.range.rc[3] + 1)
ctx.edit:set_cursor(ctx.range.rc[1] + 2, ctx.range.rc[2] + 1)
end)
end,
},
}
}
There are several helper functions to make it easier to create actions:
---@class Edit: Ctx
---@field get_lines fun(self: Edit, l_start?: number, l_end?: number): string[] - get lines in the buffer
---@field set_lines fun(self: Edit, lines: string[], l_start?: number, l_end?: number) - set lines in the buffer
---@field get_range fun(self: Edit, ls?: number, cs?: number, le?: number, ce?: number): string[] - get lines in the range of the buffer
---@field set_range fun(self: Edit, lines: string[], ls?: number, cs?: number, le?: number, ce?: number) - set lines in range of the buffer
---@field get_node fun(self: Edit, types: string|string[], node?: TSNode|nil, predicate?: fun(node: TSNode): boolean| nil): TSNode|nil, table <number, number, number, number>|nil - traverses up the tree to find the first TS node matching specified type/s
---@field get_previous_node fun(self: Edit, node: TSNode, allow_switch_parents?: boolean, allow_previous_parent?: boolean): TSNode|nil - get previous node with same parent
---@field get_node_text fun(self: Edit, node?: TSNode): string|nil - get the text of the node
---@field indent fun(self: Edit, l_start?: number, l_end?: number) - indent range in the buffer
---@field set_cursor fun(self: Edit, row?: number, col?: number) - set the cursor in the buffer
---@field write fun() - write the buffer
Note
Dev-tools actions API is slightly different from null-ls/none-ls
API.
I may implement 100% compatibility if there is a demand for it. Let me know.
This project is originally thought out as community driven.
The goal is to provide a simple and intuitive interface for creating and managing code actions, as well as a collection of useful code actions that can be used out of the box. Your contributions are highly desired and appreciated!
All actions are stored in dev-tools.nvim/lua/dev-tools/actions/
.
Actions specific to a language can be put under the relevant subdirectory.
---@class Actions
---@field group string - group of actions
---@field filetype string[]|nil - filetype to limit the actions group to
---@field actions Action[]|fun(): Action[] - list oe actions
---@class Action
---@field name string - name of the action
---@field group string|nil - group of the action
---@field condition string|nil|fun(action: ActionCtx): boolean - function or pattern to match against buffer name
---@field filetype string[]|nil - filetype to limit the action to
---@field fn fun(action: ActionCtx) - function to execute the action
---@type Actions
return {
group = "Refactoring",
filetype = { "lua" },
actions = {
{
name = "Extract variable",
condition = "_spec",
fn = function(action)
---
end,
},
{
name = "Extract function",
condition = function(action) return action.ctx.root:match("project") end,
fn = function(action)
--
end,
},
},
}
- Extract variable
- Extract function
- Split/join function/table/conditional
- Convert JSON to Lua table
- Run/watch all specs
- Run/watch current spec
- Switch between code and spec files
- Toggle pending
- Toggle #wip tag
- Log variable under cursor
- Log with trace
- Log on condition
- Log in spec
- Clear logs
Refactoring ThePrimeagen/refactoring.nvim
The module should be included as a dependency or installed separately, and will be automatically detected.
Actions are available only for visually selected code.
- Extract function
- Inline function
- Extract variable
- Inline variable
- Todo open/add
MIT