Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 108 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Describe the regular expression under the cursor.

https://user-images.githubusercontent.com/1466420/156946492-a05600dc-0a5b-49e6-9ad2-417a403909a8.mov

[railroads.webm](https://github.com/user-attachments/assets/0b17e421-3df5-4ea6-a4bc-5e32e5204216)

Heavily inspired by the venerable [atom-regexp-railroad][atom-regexp-railroad].

> 👉 **NOTE**: Requires Neovim 0.7 👈
Expand Down Expand Up @@ -38,8 +40,8 @@ Regexplainer with TypeScript sources, you need to do this:
```lua
-- defaults
require'regexplainer'.setup {
-- 'narrative'
mode = 'narrative', -- TODO: 'ascii', 'graphical'
-- 'narrative', 'graphical'
mode = 'narrative',

-- automatically show the explainer when the cursor enters a regexp
auto = false,
Expand Down Expand Up @@ -75,6 +77,19 @@ require'regexplainer'.setup {
narrative = {
indendation_string = '> ', -- default ' '
},

graphical = {
width = 800, -- image width in pixels
height = 600, -- image height in pixels
python_cmd = nil, -- python command (auto-detected)
},

deps = {
auto_install = true, -- automatically install Python dependencies
python_cmd = nil, -- python command (auto-detected)
venv_path = nil, -- virtual environment path (auto-generated)
check_interval = 3600, -- dependency check interval in seconds
},
}
```

Expand Down Expand Up @@ -176,11 +191,99 @@ You can also use the command `RegexplainerYank`
:RegexplainerYank +
```

## 🚂 Graphical Mode

Regexplainer supports displaying regular expressions as visual railroad diagrams, providing an intuitive visual representation of regex patterns that makes complex expressions easier to understand at a glance.

### Requirements

- [hologram.nvim](https://github.com/edluffy/hologram.nvim) plugin for image display
- A supported terminal (Kitty, iTerm2, or other hologram-compatible terminals)
- Python 3.7+ (dependencies are managed automatically)

### Quick Start

Set the mode to `graphical` in your configuration:

```lua
require'regexplainer'.setup {
mode = 'graphical',
-- Both popup and split modes work with graphical display
display = 'popup', -- or 'split'

graphical = {
-- Optional: customize image generation
generation_width = 1200, -- Initial generation width (default: 1200)
generation_height = 800, -- Initial generation height (default: 800)
},

deps = {
auto_install = true, -- automatically install Python dependencies
},
}
```

### Features

- **🎨 Visual railroad diagrams**: Convert regex patterns into clear, readable railroad diagrams using the `railroad-diagrams` library
- **📱 Multiple display modes**: Works in both popup windows (with pattern overlay) and split windows
- **🔧 Smart sizing**: Images automatically scale to fit your window while preserving aspect ratio
- **⚡ Caching**: Generated diagrams are cached for faster subsequent displays
- **🐍 Zero-config Python**: Dependencies are automatically managed in an isolated environment
- **📺 Wide terminal support**: Works with any terminal supported by hologram.nvim
- **🔄 Graceful fallback**: Automatically falls back to narrative mode if graphics are unavailable
- **🚀 Cross-platform**: Fully compatible with Windows, macOS, and Linux

### Display Modes

#### Popup Mode (Default)
- Railroad diagram appears in a popup window
- Original regex pattern shown in a separate overlay
- Automatically closes when cursor moves away
- Perfect for quick regex inspection

#### Split Mode
- Railroad diagram appears in a dedicated split window
- Original pattern remains visible in the main buffer
- Great for complex regex analysis and comparison

### Dependency Management

nvim-regexplainer automatically handles all Python dependencies:

- **🔄 Automatic installation**: Required packages (`railroad-diagrams`, `Pillow`, `cairosvg`) are installed when first needed
- **🏠 Isolated environment**: Uses a virtual environment in the plugin directory
- **🔒 System-safe**: Never affects your system Python installation
- **🩺 Health checks**: Run `:checkhealth regexplainer` to verify everything is working

#### Manual Dependency Management

If you prefer manual control:

```lua
require'regexplainer'.setup {
mode = 'graphical',
deps = {
auto_install = false,
python_cmd = 'python3', -- specify Python executable
venv_path = '/custom/path', -- custom virtual environment path
},
}
```

Then install the required packages:

```bash
pip install railroad-diagrams Pillow cairosvg
```

## 🗃️ TODO list
- [ ] Display Regexp [railroad diagrams][railroad-diagrams] using ASCII-art
- [ ] Display Regexp [railroad diagrams][railroad-diagrams] via
[hologram][hologram] and [kitty image protocol][kitty], maybe with a sixel
fallback
- [x] Display Regexp [railroad diagrams][railroad-diagrams] via [hologram.nvim][hologram]
- [x] Support both popup and split display modes for graphical diagrams
- [x] Automatic Python dependency management
- [x] Cross-platform compatibility (Windows, macOS, Linux)
- [ ] Add sixel protocol support for wider terminal compatibility
- [ ] online documentation
- [x] some unit tests or something, i guess

Expand Down
73 changes: 58 additions & 15 deletions lua/regexplainer.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
local component = require 'regexplainer.component'
local tree = require 'regexplainer.utils.treesitter'
local utils = require 'regexplainer.utils'
local Buffers = require 'regexplainer.buffers'
local tree = require 'regexplainer.utils.treesitter'
local utils = require 'regexplainer.utils'
local Buffers = require 'regexplainer.buffers'
local cache = require 'regexplainer.cache'

local get_node_text = vim.treesitter.get_node_text
local deep_extend = vim.tbl_deep_extend
Expand All @@ -11,13 +12,15 @@ local ag = vim.api.nvim_create_augroup
local au = vim.api.nvim_create_autocmd

---@class RegexplainerOptions
---@field mode? 'narrative'|'debug' # TODO: 'ascii', 'graphical'
---@field mode? 'narrative'|'debug'|'graphical' # Renderer mode
---@field auto? boolean # Automatically display when cursor enters a regexp
---@field filetypes? string[] # Filetypes (extensions) to automatically show regexplainer.
---@field debug? boolean # Notify debug logs
---@field display? 'split'|'popup'
---@field mappings? RegexplainerMappings # keymappings to automatically bind.
---@field narrative? RegexplainerNarrativeRendererOptions # Options for the narrative renderer
---@field graphical? RegexplainerGraphicalRendererOptions # Options for the graphical renderer
---@field deps? RegexplainerDepsConfig # Options for dependency management
---@field popup? NuiPopupBufferOptions # options for the popup buffer
---@field split? NuiSplitBufferOptions # options for the split buffer

Expand All @@ -38,9 +41,10 @@ local au = vim.api.nvim_create_autocmd
---Maps config.mappings keys to vim command names and descriptions
--
local config_command_map = {
show = { 'RegexplainerShow', 'Show Regexplainer' },
hide = { 'RegexplainerHide', 'Hide Regexplainer' },
toggle = { 'RegexplainerToggle', 'Toggle Regexplainer' }, yank = { 'RegexplainerYank', 'Yank Regexplainer' },
show = { 'RegexplainerShow', 'Show Regexplainer' },
hide = { 'RegexplainerHide', 'Hide Regexplainer' },
toggle = { 'RegexplainerToggle', 'Toggle Regexplainer' },
yank = { 'RegexplainerYank', 'Yank Regexplainer' },
show_split = { 'RegexplainerShowSplit', 'Show Regexplainer in a split Window' },
show_popup = { 'RegexplainerShowPopup', 'Show Regexplainer in a popup' },
}
Expand All @@ -54,10 +58,22 @@ local default_config = {
auto = false,
filetypes = {
'html',
'js', 'javascript', 'cjs', 'mjs',
'ts', 'typescript', 'cts', 'mts',
'tsx', 'typescriptreact', 'ctsx', 'mtsx',
'jsx', 'javascriptreact', 'cjsx', 'mjsx',
'js',
'javascript',
'cjs',
'mjs',
'ts',
'typescript',
'cts',
'mts',
'tsx',
'typescriptreact',
'ctsx',
'mtsx',
'jsx',
'javascriptreact',
'cjsx',
'mjsx',
},
debug = false,
display = 'popup',
Expand All @@ -67,6 +83,17 @@ local default_config = {
narrative = {
indentation_string = ' ',
},
graphical = {
width = 800,
height = 600,
python_cmd = nil, -- Will be auto-detected
},
deps = {
auto_install = true,
python_cmd = nil, -- Will be auto-detected
venv_path = nil, -- Will be auto-generated
check_interval = 3600,
},
}

--- A deep copy of the default config.
Expand Down Expand Up @@ -100,10 +127,17 @@ local function show_for_real(options)
local buffer = Buffers.get_buffer(options)

if not buffer and options.debug then
renderer = require'regexplainer.renderers.debug'
renderer = require 'regexplainer.renderers.debug'
end

local state = { full_regexp_text = get_node_text(node, scratchnr) }
local start_row, start_col, end_row, end_col = node:range()
local state = {
full_regexp_text = get_node_text(node, scratchnr),
full_regexp_range = {
start = { row = start_row, column = start_col },
finish = { row = end_row, column = end_col },
},
}

Buffers.render(buffer, renderer, components, options, state)
buf_delete(scratchnr, { force = true })
Expand Down Expand Up @@ -154,7 +188,9 @@ function M.setup(config)
ag(augroup_name, { clear = true })
au('CursorMoved', {
group = 'Regexplainer',
pattern = map(function(x) return '*.' .. x end, local_config.filetypes),
pattern = map(function(x)
return '*.' .. x
end, local_config.filetypes),
callback = function()
if tree.has_regexp_at_cursor() and not disable_auto then
show_for_real()
Expand Down Expand Up @@ -195,7 +231,14 @@ end
function M.debug_components()
---@type any
local mode = 'debug'
show_for_real({ auto = false, display = 'split', mode = mode })
show_for_real { auto = false, display = 'split', mode = mode }
end

--- Clear the image cache
--
function M.clear_cache()
cache.clear_cache()
utils.notify('Regexplainer image cache cleared', 'info')
end

return M
5 changes: 5 additions & 0 deletions lua/regexplainer/buffers/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ function M.render(buffer, renderer, components, options, state)
buffer:init(lines, options, state)
renderer.set_lines(buffer, lines)
buffer:after(lines, options, state)

-- Call renderer-specific after hook if available
if renderer.after_render then
renderer.after_render(buffer, lines, options, state)
end
end

--- Close and unload a buffer
Expand Down
Loading