A simple pattern explainer & editor for Neovim.
-
Tree-sitter based pattern explainer.
-
LSP-like hover window for strings.
-
A real-time pattern editor & matcher.
-
Support for multiple pattern languages,
regex
lua_patterns
(requires custom parser)
-
Highly configurable! Almost everything can be configured(without needing to leave your editor).
-
Tree-sitter parser.
regex
(install throughnvim-treesitter
via:TSInstall regex
)lua_patterns
(optional, See parser installation).
-
A tree-sitter supported colorscheme(optional).
-
A nerd font.
-
Node.js(optional, needed if you use javascript's regexp).
Add this to your plugin list.
Plug "OXY2DEV/patterns.nvim"
Note
Lazy loading is NOT needed for this plugin!
For plugins.lua
users,
{
"OXY2DEV/patterns.nvim",
},
For plugins/patterns.lua
,
return {
"OXY2DEV/patterns.nvim",
};
local MiniDeps = require("mini.deps");
MiniDeps.add({
source = "OXY2DEV/patterns.nvim"
});
Warning
luarocks package
may sometimes be a bit behind main
.
:Rocks install patterns.nvim
Tagged releases can be found in the release page.
Note
Github releases
may sometimes be slightly behind main
.
See the default configuration here.
Show type definitions
--- Configuration for `patterns.nvim`.
---@class patterns.config
---
--- WARNING, This just changes the priority
--- of the matchers.
--- Both matchers will be tried!
---@field preferred_regex_matcher
---| "node" Allows matching Javascript's regexp.
---| "vim" Allows matching Vim's regexp.
---
--- Delay for updating explainer UI.
---@field update_delay integer
---
---@field keymaps? patterns.keymaps
---@field windows patterns.windows
---@field lua_patterns patterns.lua_patterns
---@field regex patterns.regex
---@class patterns.keymaps
---
---@field hover table<string, patterns.keymap_opts>
---@field explain_input table<string, patterns.keymap_opts>
---@field explain_preview table<string, patterns.keymap_opts>
--- Action names for the explainer.
---@alias explain_actions
---| "toggle" Toggle focus of window.
---| "mode_change" Switches between the explainer & the matcher.
---
--- Changes pattern language backwards.
---| "lang_prev"
---| "lang_next" Changes pattern language forwards.
---
--- Closes explainer.
---| "close"
---| "apply" Applies changes.
--- Action names for the hover.
---@alias hover_actions
---| "close" Closes hover window.
---| "edit" Edit pattern.
---@class patterns.keymap_opts
---
---@field desc? string
---@field callback explain_actions | hover_actions | function
--- Window configurations for various
--- windows.
---@class patterns.windows
---
---@field hover? table | fun(q1: "left" | "right" | "center", q2: "top" | "bottom" | "center"): table
---
---@field input? table | fun(): table
---@field preview? table | fun(): table
--- Options for Lua patterns.
--- Option name matches the tree-sitter node name.
---@class patterns.lua_patterns
---
---@field indent_size integer Indentation size.
---@field indent_marker string Marker used for indentation.
---@field indent_hl? string Highlight group for the indentation markers.
---
---@field pattern pattern_item.opts
---
---@field anchor_start pattern_item.opts
---@field anchor_end pattern_item.opts
---
---@field quantifier_optional pattern_item.opts
---@field quantifier_minus pattern_item.opts
---@field quantifier_plus pattern_item.opts
---@field quantifier_star pattern_item.opts
---
---@field literal_character pattern_item.opts
---@field any_character pattern_item.opts
---@field escape_sequence pattern_item.opts
---@field escaped_character pattern_item.opts
---
---@field capture_group pattern_item.opts
---@field character_set pattern_item.opts
---@field character_set_content pattern_item.opts
---@field character_range pattern_item.opts
---@field character_class pattern_item.opts
--- Options for Regex.
--- Option name matches the tree-sitter node name.
---@class patterns.regex
---
---@field indent_size integer Indentation size.
---@field indent_marker string Marker used for indentation.
---@field indent_hl? string Highlight group for the indentation markers.
---
---@field pattern pattern_item.opts
---@field alternation pattern_item.opts
---@field term pattern_item.opts
---
---@field start_assertion pattern_item.opts
---@field end_assertion pattern_item.opts
---@field boundary_assertion pattern_item.opts
---@field non_boundary_assertion pattern_item.opts
---@field lookaround_assertion pattern_item.opts
---
---@field quantifier_count pattern_item.opts
---@field quantifier_optional pattern_item.opts
---@field quantifier_plus pattern_item.opts
---@field quantifier_star pattern_item.opts
---
---@field pattern_character pattern_item.opts
---@field class_character pattern_item.opts
---@field any_character pattern_item.opts
---@field decimal_escape pattern_item.opts
---@field character_class_escape pattern_item.opts
---@field unicode_character_escape pattern_item.opts
---@field unicode_property_value pattern_item.opts
---@field control_escape pattern_item.opts
---@field control_letter_escape pattern_item.opts
---@field identity_escape pattern_item.opts
---@field backreference_escape pattern_item.opts
---@field unicode_property_value_expression pattern_item.opts
---
---@field character_class pattern_item.opts
---@field posix_character_class pattern_item.opts
---@field named_group_backreference pattern_item.opts
---@field capturing_group pattern_item.opts
---@field non_capturing_group pattern_item.opts
---
---@field flags_group pattern_item.opts
---@field flags pattern_item.opts
--- Options for each node type.
---@class pattern_item.opts
---
--- Can be set to `false` to disable rendering of
--- a specific node type.
---@field enable? boolean | fun(buffer: integer, item: __patterns.item): boolean
---
--- Can be set to `true` to show the range of a
--- node.
---@field show_range? boolean | fun(buffer: integer, item: __patterns.item): boolean
---
--- Highlight group for the text.
---@field text_hl? string | fun(buffer: integer, item: __patterns.item): string?
---
--- Text to show for a node.
---@field text? string | fun(buffer: integer, item: __patterns.item): string
---
--- When set to `true`, shows tooltip for nodes.
--- By default this only shows tips for the current
--- node.
---@field show_tip? boolean | fun(buffer: integer, item: __patterns.item): boolean
---
--- Highlight group for the tooltip text.
---@field tip_hl? string | fun(buffer: integer, item: __patterns.item): string
---
--- Number of spaces to add before tooltip text.
--- This is added AFTER the indentation.
---@field tip_offset? integer | fun(buffer: integer, item: __patterns.item): integer
---
--- Highlight group for the node range.
---@field range_hl? string | fun(buffer: integer, item: __patterns.item): string?
---
--- Bade highlight group. Used by other *_hl
--- options when they don't have a value.
---@field hl? string | fun(buffer: integer, item: __patterns.item): string?
Show default configuration
spec.default = {
preferred_regex_matcher = "vim",
update_delay = 150,
keymaps = {
explain_input = {
["<CR>"] = {
callback = "apply"
},
["q"] = {
callback = "close"
},
["<tab>"] = {
callback = "toggle"
},
["H"] = {
callback = "lang_prev"
},
["L"] = {
callback = "lang_next"
},
},
explain_preview = {
["q"] = {
callback = "close"
},
["<tab>"] = {
callback = "toggle"
},
["T"] = {
callback = "mode_change"
}
},
hover = {
["q"] = {
callback = "close"
},
["i"] = {
callback = "edit"
}
}
},
windows = {
hover = function (q1, q2)
local border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" };
if q2 == "top" then
if q1 == "left" then
border[5] = "┤";
elseif q1 == "right" then
border[7] = "├";
end
elseif q2 == "bottom" then
if q1 == "left" then
border[3] = "┤";
elseif q1 == "right" then
border[1] = "├";
end
end
local ft;
if package.loaded["patterns.hover"] and package.loaded["patterns.hover"].buf then
ft = vim.bo[package.loaded["patterns.hover"].buf].ft;
end
return {
width = math.floor(vim.o.columns * 0.6),
height = math.floor(vim.o.lines * 0.5),
border = border,
footer_pos = "right",
footer = {
{ "╸", "FloatBorder" },
{ " " .. (ft or "Patterns") .. " ", "FloatBorder" },
{ "╺", "FloatBorder" },
}
}
end
},
lua_patterns = {
indent_size = 2,
indent_marker = "│",
indent_hl = "PatternsPalette0Fg",
pattern = {
text = " Pattern",
show_tip = on_current,
tip_hl = "PatternsPalette0Bg",
hl = "PatternsPalette0";
},
----------------------------------------
anchor_start = {
text = " From start",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
anchor_end = {
text = " To end",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
----------------------------------------
quantifier_minus = {
text = " Zero or more times(non-greedily)",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
quantifier_optional = {
text = " Zero or one time",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
quantifier_plus = {
text = " One or more times",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
quantifier_star = {
text = " Zero or more times(greedily)",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
----------------------------------------
literal_character = {
text = function (_, item)
if item.text == "\\" then
return ' Character: "\\"';
else
return string.format(" Character: %s", vim.inspect(item.text));
end
end,
show_tip = on_current,
tip_hl = "PatternsPalette4Bg",
hl = "PatternsPalette4"
},
any_character = {
text = " Any character",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
escape_sequence = {
text = function (_, item)
return string.format(' Escape sequence: "%s"', item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
escaped_character = {
text = function (_, item)
return string.format(' Escaped character: "%s"', item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette6Bg",
hl = "PatternsPalette6"
},
----------------------------------------
capture_group = {
text = function (_, item)
return string.format(" Capture group, %d", item.id or -1);
end,
show_tip = on_current,
tip_hl = "PatternsPalette6Bg",
hl = "PatternsPalette6"
},
character_set = {
text = " Character set",
show_tip = on_current,
tip_hl = "PatternsPalette3Bg",
hl = "PatternsPalette3"
},
character_set_content = {
text = " Character set content,",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
character_range = {
text = function (_, item)
return string.format(" Character range: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette6Bg",
hl = "PatternsPalette6"
},
character_class = {
text = function (_, item)
return " Character class: " .. vim.inspect(item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette4Bg",
hl = "PatternsPalette4"
},
},
regex = {
indent_size = 2,
indent_marker = "│",
indent_hl = "PatternsPalette0Fg",
pattern = {
text = " Pattern",
show_tip = on_current,
tip_hl = "PatternsPalette0Bg",
hl = "PatternsPalette0"
},
alternation = {
text = " Alternative pattern(s)",
show_tip = on_current,
tip_hl = "PatternsPalette6Bg",
hl = "PatternsPalette6"
},
term = {
text = function (_, item)
return string.format(" Regex term(#%d)", item.id or -1);
end,
show_tip = on_current,
tip_hl = "PatternsPalette6Bg",
hl = "PatternsPalette6"
},
----------------------------------------
start_assertion = {
text = " From start",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
end_assertion = {
text = " To end",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
boundary_assertion = {
text = " Match as a word",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
non_boundary_assertion = {
text = " Match as part of a word",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
lookaround_assertion = {
text = function (_, item)
if string.match(item.text, "^%(%?%<") then
return " Look behind";
else
return " Look ahead";
end
end,
show_tip = on_current,
tip_hl = "PatternsPalette3Bg",
hl = "PatternsPalette3"
},
----------------------------------------
quantifier_count = {
text = function (_, item)
if string.match(item.text, "^%d+$") then
return string.format(" Repeats exactly %s times", item.text);
elseif string.match(item.text, "^%d+,$") then
return string.format(
" Repeats at least %s times",
string.match(item.text, "^(%d+)")
);
elseif string.match(item.text, "^,%d+$") then
return string.format(
" Repeats at most %s times",
string.match(item.text, "^,(%d+)$")
);
else
return string.format(
" Repeats between %s & %s times",
string.match(item.text, "^(%d+),"),
string.match(item.text, "^%d+,(%d+)$")
);
end
end,
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
quantifier_optional = {
text = " Repeats zero or one time",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
quantifier_plus = {
text = " Repeats one or more times",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
quantifier_star = {
text = " Repeats zero or more times",
show_tip = on_current,
tip_hl = "PatternsPalette7Bg",
hl = "PatternsPalette7"
},
----------------------------------------
pattern_character = {
text = function (_, item)
return string.format(" Character: %s", vim.inspect(item.text));
end,
show_tip = on_current,
tip_hl = "PatternsPalette2Bg",
hl = "PatternsPalette2"
},
class_character = {
text = function (_, item)
return string.format(" Character: %s", vim.inspect(item.text));
end,
show_tip = on_current,
tip_hl = "PatternsPalette2Bg",
hl = "PatternsPalette2"
},
any_character = {
text = " Any character",
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
decimal_escape = {
text = function (_, item)
return string.format(" Decimal escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
character_class_escape = {
text = function (_, item)
return string.format(" Character class escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
unicode_character_escape = {
text = function (_, item)
return string.format(" Unicode character escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
unicode_property_value = {
text = function (_, item)
return string.format(" Unicode property value: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette6Bg",
hl = "PatternsPalette6"
},
control_escape = {
text = function (_, item)
return string.format(" Control character escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
control_letter_escape = {
text = function (_, item)
return string.format(" Control letter escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
identity_escape = {
text = function (_, item)
return string.format(" Identity escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
backreference_escape = {
text = function (_, item)
return string.format(" Backreference escape: %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette1Bg",
hl = "PatternsPalette1"
},
----------------------------------------
unicode_property_value_expression = {
text = " Unicode property value expression",
show_tip = on_current,
-- show_content = true,
tip_hl = "PatternsPalette0Bg",
hl = "PatternsPalette0"
},
----------------------------------------
character_class = {
text = " Character class",
show_tip = on_current,
tip_hl = "PatternsPalette4Bg",
hl = "PatternsPalette4"
},
posix_character_class = {
text = function (_, item)
return string.format(" POSIX Character class: ", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
named_group_backreference = {
text = function (_, item)
return string.format(" Named backreference: ", string.match(item.text, "^%(%?P%=(.-)%)$"));
end,
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
capturing_group = {
text = function (_, item)
if type(item.id) == "string" then
return string.format(" Capture group(#%s)", item.id or "???");
else
return string.format(" Capture group(#%d)", item.id or -1);
end
end,
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
non_capturing_group = {
text = function (_, item)
return string.format(" Non-capture group(#%d)", item.id or -1);
end,
show_tip = on_current,
tip_hl = "PatternsPalette5Bg",
hl = "PatternsPalette5"
},
flags_group = {
text = " Flags group",
show_tip = on_current,
tip_hl = "PatternsPalette2Bg",
hl = "PatternsPalette2"
},
flags = {
text = function (_, item)
return string.format(" Flag(s): %s", item.text);
end,
show_tip = on_current,
tip_hl = "PatternsPalette2Bg",
hl = "PatternsPalette2"
},
}
};
This plugin creates the :Patterns
command. It has 2 sub-commands,
-
explain
Explains the pattern under the cursor. -
hover
LSP-like hover for the pattern under cursor. It's behavior is similar toK
(orvim.lsp.buf.hover()
).
When :Patterns
is run without any arguments, it opens the explain window.
The hover
& explain
buffers have some pre-defined keymaps.
Tip
You can disable these keymaps individually via the config.
{
keymaps = {
hover = {
["i"] = { enable = false }
}
}
}
The hover buffer has the following keymaps,
-
q
Closes hover window. -
i
Opens the pattern inside the explainer.
The text input buffer has the following keymaps,
-
<CR>
Replaces the pattern under cursor with the text in the input buffer. -
q
Quits the explainer. -
<tab>
Switches to the explanation/preview buffer. -
H
Cycles backward through the list of supported languages(lua_patterns, regex). -
L
Cycles forward through the list of supported languages(lua_patterns, regex).
The pattern preview/explanation buffer has the following keymaps,
-
q
Quits the explainer. -
<tab>
Switches to the input buffer. -
T
Toggles between the explanation and the matcher.