Skip to content

Commit

Permalink
Merge pull request #34 from smjonas/rewrite
Browse files Browse the repository at this point in the history
Rewrite for better architecture, easier extensibility; add a general :Preview command
  • Loading branch information
smjonas authored Sep 15, 2024
2 parents 79f89a2 + ee22428 commit 558a0f1
Show file tree
Hide file tree
Showing 13 changed files with 795 additions and 475 deletions.
115 changes: 60 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,77 +1,82 @@
# live-command.nvim
![version](https://img.shields.io/badge/version-1.2.1-brightgreen)
![version](https://img.shields.io/badge/version-2.0.0-brightgreen)

Text editing in Neovim with immediate visual feedback: view the effects of any command on your buffer contents live. Preview macros, the `:norm` command & more!
> :exclamation: Version 2.0 has been released with breaking changes! Be sure to check out the [migration guide](./migrate_to_v2.md).
Text editing in Neovim with immediate visual feedback: see the effects of any command on your buffer in real-time. Preview macros, the `:norm` command, and more!

![live-command.nvim demo video](https://user-images.githubusercontent.com/40792180/235201812-adc95327-65cc-4ae4-8c2e-804853dd0c02.gif)
<p><sub>Theme: <a href="https://github.com/folke/tokyonight.nvim">tokyonight.nvim</a></sub></p>

## :sparkles: Motivation and Features
In version 0.8, Neovim has introduced the `command-preview` feature.
Contrary to what "command preview" suggests, previewing any given
command does not work out of the box: you need to manually update the buffer text and set
highlights *for every command*.
In Neovim version 0.8, the `command-preview` feature was introduced.
Despite its name, it does not enable automatic previewing of any command.
Instead, users must manually update the buffer text and set highlights *for each command* they wish to preview.

This plugin tries to change that: it provides a **simple API for creating previewable commands**
in Neovim. Just specify the command you want to run and live-command will do all the
work for you. This includes viewing **individual insertions, changes and deletions** as you
type.
This plugin addresses that limitation by offering a **simple API for creating previewable commands**
in Neovim. Just specify the command you want to preview and live-command will handle the rest.
This includes viewing **individual insertions, changes and deletions** as you type.

## Requirements
Neovim 0.8+

## :rocket: Getting started
Install using your favorite package manager and call the setup function with a table of
commands to create. Here is an example that creates a previewable `:Norm` command:
## :inbox_tray: Installation
Install via your favorite package manager and call the `setup` function:

<details>
<summary>lazy.nvim</summary>

```lua
use {
"smjonas/live-command.nvim",
-- live-command supports semantic versioning via tags
-- tag = "1.*",
-- live-command supports semantic versioning via Git tags
-- tag = "2.*",
config = function()
require("live-command").setup {
commands = {
Norm = { cmd = "norm" },
},
}
require("live-command").setup()
end,
}
```
</details>

## :gear: Usage and Customization
Each command you want to preview requires a name (must be upper-case) and the name of
an existing command that is run on each keypress.
<details>
<summary>vim-plug</summary>

Here is a list of available settings:

| Key | Type | Description
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------
| cmd | string | The name of an existing command to preview.
| args | string? \| function(arg: string?, opts: table) -> string | Arguments passed to the command. If a function, takes in the options passed to the command and must return the transformed argument(s) `cmd` will be called with. `opts` has the same structure as the `opts` table passed to the `nvim_create_user_command` callback function. If `nil`, the arguments are supplied from the command-line while the user is typing the command.
| range | string? | The range to prepend to the command. Set this to `""` if you don't want the new command to receive a count, e.g. when turning `:9Reg a` into `:norm 9@a`. If `nil`, the range will be supplied from the command entered.

### Example
The following example creates a `:Reg` command which allows you to preview the effects of macros (e.g. `:5Reg a` to run macro `a` five times).
```vim
Plug 'smjonas/live-command.nvim'
```
In your `init.lua`, call the setup function:
```lua
local commands = {
Reg = {
cmd = "norm",
-- This will transform ":5Reg a" into ":norm 5@a"
args = function(opts)
return (opts.count == -1 and "" or opts.count) .. "@" .. opts.args
end,
range = "",
},
}
require("live-command").setup()
```
</details>

## :rocket: Getting started
### Basic Usage
The easiest way to use **live-command** is with the provided `:Preview` command.
For example, `:Preview delete` will show you a preview of deleting the current line.
You can also provide a count or a range to the command, such as `:'<,'>Preview norm A;`, which
shows the effect of appending a semicolon to every visually selected line.

### Creating Previewable Commands
For a more convenient experience, **live-command** allows you to define custom previewable commands.
This can be done by passing a list of commands to the `setup` function.
For instance, to define a custom `:Norm` command that can be previewed, use the following:
```lua
require("live-command").setup {
commands = commands,
commands = {
Norm = { cmd = "norm" },
},
}
```
\
All of the following options can be set globally (for all created commands), or per command.

To change the default options globally, use the `defaults` table. The defaults are:
Each command you want to preview needs a name (which must be uppercase) and
an existing command to run on each keypress, specified via the `cmd` field.

## :gear: Customization

All of the following options can be set globally (affecting all custom commands), or per command.

To change the default options globally, use the `defaults` table. The default settings are:

```lua
require("live-command").setup {
Expand All @@ -93,28 +98,28 @@ require("live-command").setup {

Default: `true`

Whether highlights should be shown. If `false`, only text changes are shown.
Determines whether highlights should be shown. If `false`, only text changes are shown, without any highlights.

---

`inline_highlighting: boolean`

Default: `true`

If `true`, differing lines will be compared in a second run of the diff algorithm. This
can result in multiple highlights per line. Otherwise, the whole line will be highlighted as
a single change highlight.
If `true`, differing lines will be compared in a second run of the diff algorithm
to identify smaller differences. This can result in multiple highlights per line.
If set to `false`, the whole line will be highlighted as a single change.

---

`hl_groups: table<string, string|boolean>`

Default: `{ insertion = "DiffAdd", deletion = "DiffDelete", change = "DiffChange" }`

A list of highlight groups per edit type (insertion, deletion or change) used for highlighting buffer changes.
The table will be merged with the defaults so you can omit any keys that are the same as the default.
If a value is set to `false`, no highlights will be shown for that type. If `hl_groups.deletion` is `false`,
deletion edits will not be undone which is otherwise done to make the text changes visible.
A table mapping edit types (insertion, deletion or change) to highlight groups used for highlighting buffer changes.
This table is merged with the defaults, allowing you to omit any keys that match the default.
If a value is set to `false`, no highlights will be shown for that type.
If `hl_groups.deletion` is `false`, deletion edits will not be undone, so deleted text won't be highlighted.

---

Expand Down
86 changes: 86 additions & 0 deletions lua/live-command/cmd_executor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
local M = {}

local differ = require("live-command.differ")
local highlighter = require("live-command.highlighter")
local logger = require("live-command.logger")

---@type string
local latest_cmd

local running = false

local refetch_lines = true

---@tyle string[]
local cached_lines

---@type boolean
local prev_lazyredraw

local setup = function(bufnr)
prev_lazyredraw = vim.o.lazyredraw
vim.o.lazyredraw = true
if refetch_lines then
cached_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
refetch_lines = false
else
logger.trace("did not refetch for cmd " .. latest_cmd)
end
return cached_lines
end

M.teardown = function(do_refetch_lines)
vim.o.lazyredraw = prev_lazyredraw
refetch_lines = do_refetch_lines
if vim.v.errmsg ~= "" then
logger.error(("An error occurred in the preview function:\n%s"):format(vim.inspect(vim.v.errmsg)))
end
end

local execute_command = function(cmd, bufnr)
local old_buf_lines = setup(bufnr)
local visible_line_range = { vim.fn.line("w0"), vim.fn.line("w$") }
vim.api.nvim_buf_call(bufnr, function()
vim.cmd(cmd)
end)
-- M.teardown(false)
visible_line_range = {
math.max(visible_line_range[1], vim.fn.line("w0")),
math.max(visible_line_range[2], vim.fn.line("w$")),
}
local new_buf_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
return old_buf_lines, new_buf_lines, visible_line_range
end

---@param cmd string
---@param opts livecmd.Config
---@param bufnr number
---@param update_buffer_cb fun(bufnr:number,updated_buffer_lines:string[],highlights:livecmd.Highlight[]?)
M.submit_command = function(cmd, opts, bufnr, update_buffer_cb)
if cmd == latest_cmd then
return
end
latest_cmd = cmd
if not running then
running = true
local old_buf_lines, new_buf_lines, line_range = execute_command(cmd, bufnr)
if not opts.enable_highlighting then
update_buffer_cb(bufnr, new_buf_lines, nil)
running = false
return
end
local diff = differ.get_diff(old_buf_lines, new_buf_lines)
local highlights, updated_buf_lines = highlighter.get_highlights(
diff,
old_buf_lines,
new_buf_lines,
line_range,
opts.inline_highlighting,
opts.hl_groups.deletion ~= false
)
update_buffer_cb(bufnr, updated_buf_lines, highlights)
running = false
end
end

return M
56 changes: 56 additions & 0 deletions lua/live-command/config_validator.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
local M = {}

local user_command = require("live-command.user_command")

---@class livecmd.Config.HlGroups
---@field insertion string|false
---@field deletion string|false
---@field change string|false

---@class livecmd.Config
---@field command_name string?
---@field enable_highlighting boolean?
---@field inline_highlighting boolean?
---@field hl_groups livecmd.Config.HlGroups?
---@field commands table<string, livecmd.CommandSpec>

local show_diagnostics_message = function(config)
local message = [[
Version 2.0 of live-command.nvim has dropped support for the "args" and "range" keys in the command specification.
The following commands in your configuration are affected: %s. Please remove or modify them.
See the migration guide for more information: https://github.com/smjonas/live-command.nvim/blob/main/migrate_to_v2.md
]]
local affected_cmds = {}
for cmd_name, cmd_spec in pairs(config.commands) do
if cmd_spec.args ~= nil or cmd_spec.range ~= nil then
table.insert(affected_cmds, '"' .. cmd_name .. '"')
end
end
local cmd_names = table.concat(affected_cmds, ", ")
local formatted_message = string.format(message, cmd_names)
vim.notify(formatted_message, vim.log.levels.INFO)
end

---@param config livecmd.Config
M.validate_config = function(config)
vim.validate {
command_name = { config.command_name, "string" },
enable_highlighting = { config.enable_highlighting, "boolean" },
inline_highlighting = { config.inline_highlighting, "boolean" },
hl_groups = { config.hl_groups, "table" },
commands = { config.commands, "table" },
}
for cmd_name, cmd_spec in pairs(config.commands) do
if cmd_spec.args ~= nil or cmd_spec.range ~= nil then
vim.notify(
'[live-command.nvim] Some unsupported features are used in your config. Please run ":LiveCommand diagnose" for details.',
vim.log.levels.WARN
)
user_command.register_argument_handler("diagnose", function()
show_diagnostics_message(config)
end)
end
end
end

return M
9 changes: 9 additions & 0 deletions lua/live-command/differ.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
local M = {}

M.get_diff = function(old_lines, new_lines)
return vim.diff(table.concat(old_lines, "\n"), table.concat(new_lines, "\n"), {
result_type = "indices",
})
end

return M
Loading

0 comments on commit 558a0f1

Please sign in to comment.