All checks were successful
NeoVim tests / plenary-tests (push) Successful in 10s
440 lines
14 KiB
Markdown
440 lines
14 KiB
Markdown
# u.nvim
|
||
|
||
Welcome to **u.nvim** – a powerful Lua library designed to enhance your text
|
||
manipulation experience in NeoVim, focusing on text-manipulation utilities.
|
||
This includes a `Range` utility, allowing you to work efficiently with text
|
||
selections based on various conditions, as well as a declarative `Render`-er,
|
||
making coding and editing more intuitive and productive.
|
||
|
||
This is meant to be used as a **library**, not a plugin. On its own, `u.nvim`
|
||
does nothing. It is meant to be used by plugin authors, to make their lives
|
||
easier based on the variety of utilities I found I needed while growing my
|
||
NeoVim config. To get an idea of what a plugin built on top of `u.nvim` would
|
||
look like, check out the [examples/](./examples/) directory.
|
||
|
||
## Features
|
||
|
||
- **Rendering System**: a utility that can declaratively render NeoVim-specific
|
||
hyperscript into a buffer, supporting creating/managing extmarks, highlights,
|
||
and key-event handling (requires NeoVim >0.11)
|
||
- **Signals**: a simple dependency tracking system that pairs well with the
|
||
rendering utilities for creating reactive/interactive UIs in NeoVim.
|
||
- **Range Utility**: Get context-aware selections with ease. Replace regions
|
||
with new text. Think of it as a programmatic way to work with visual
|
||
selections (or regions of text).
|
||
- **Code Writer**: Write code with automatic indentation and formatting.
|
||
- **Operator Key Mapping**: Flexible key mapping that works with the selected
|
||
text.
|
||
- **Text and Position Utilities**: Convenient functions to manage text objects
|
||
and cursor positions.
|
||
|
||
### Installation
|
||
|
||
This being a library, and not a proper plugin, it is recommended that you
|
||
vendor the specific version of this library that you need, including it in your
|
||
code. Package managers are a developing landscape for Lua in the context of
|
||
NeoVim. Perhaps in the future, `lux` will eliminate the need to vendor this
|
||
library in your application code.
|
||
|
||
## Signal and Rendering Usage
|
||
|
||
### Overview
|
||
|
||
The Signal and Rendering mechanisms are two subsystems of u.nvim, that, while
|
||
simplistic, [compose](./examples/counter.lua) [together](./examples/filetree.lua)
|
||
[powerfully](./examples/picker.lua) to create a system for interactive and
|
||
responsive user interfaces. Here is a quick example that show-cases how easy it
|
||
is to dive in to make any buffer an interactive UI:
|
||
|
||
<details>
|
||
<summary>Example Code: counter.lua</summary>
|
||
|
||
```lua
|
||
local tracker = require 'u.tracker'
|
||
local Buffer = require 'u.buffer'
|
||
local h = require('u.renderer').h
|
||
|
||
-- Create an buffer for the UI
|
||
vim.cmd.vnew()
|
||
local ui_buf = Buffer.current()
|
||
ui_buf:set_tmp_options()
|
||
|
||
local s_count = tracker.create_signal(0)
|
||
|
||
-- Effect: Render
|
||
-- Setup the effect for rendering the UI whenever dependencies are updated
|
||
tracker.create_effect(function()
|
||
-- Calling `Signal:get()` in an effect registers the given signal as a
|
||
-- dependency of the current effect. Whenever that signal (or any other
|
||
-- dependency) changes, the effect will rerun. In this particular case,
|
||
-- rendering the UI is an effect that depends on one signal.
|
||
local count = s_count:get()
|
||
|
||
-- Markup is hyperscript, which is just 1) text, and 2) tags (i.e.,
|
||
-- constructed with `h(...)` calls). To help organize the markup, text and
|
||
-- tags can be nested in tables at any depth. Line breaks must be specified
|
||
-- manually, with '\n'.
|
||
ui_buf:render {
|
||
'Reactive Counter Example\n',
|
||
'========================\n\n',
|
||
|
||
{ 'Counter: ', tostring(count), '\n' },
|
||
|
||
'\n',
|
||
|
||
{
|
||
h('text', {
|
||
hl = 'DiffDelete',
|
||
nmap = {
|
||
['<CR>'] = function()
|
||
-- Update the contents of the s_count signal, notifying any
|
||
-- dependencies (in this case, the render effect):
|
||
vim.schedule(function()
|
||
s_count:update(function(n) return n - 1 end)
|
||
end)
|
||
-- Also equivalent: s_count:set(s_count:get() - 1)
|
||
return ''
|
||
end,
|
||
},
|
||
}, ' Decrement '),
|
||
' ',
|
||
h('text', {
|
||
hl = 'DiffAdd',
|
||
nmap = {
|
||
['<CR>'] = function()
|
||
-- Update the contents of the s_count signal, notifying any
|
||
-- dependencies (in this case, the render effect):
|
||
vim.schedule(function()
|
||
s_count:update(function(n) return n + 1 end)
|
||
end)
|
||
-- Also equivalent: s_count:set(s_count:get() - 1)
|
||
return ''
|
||
end,
|
||
},
|
||
}, ' Increment '),
|
||
},
|
||
|
||
'\n',
|
||
'\n',
|
||
{ 'Press <CR> on each "button" above to increment/decrement the counter.' },
|
||
}
|
||
end)
|
||
```
|
||
|
||
</details>
|
||
|
||
### `u.tracker`
|
||
|
||
The `u.tracker` module provides a simple API for creating reactive variables.
|
||
These can be composed in Effects and Memos utilizing Execution Contexts that
|
||
track what signals are used by effects/memos.
|
||
|
||
```lua
|
||
local tracker = require('u.tracker')
|
||
|
||
local s_number = tracker.Signal:new(0)
|
||
-- auto-compute the double of the number each time it changes:
|
||
local s_doubled = tracker.create_memo(function() return s_number:get() * 2 end)
|
||
tracker.create_effect(function()
|
||
local n = s_doubled:get()
|
||
-- ...
|
||
-- whenever s_doubled changes, this function gets run
|
||
end)
|
||
```
|
||
|
||
**Note**: circular dependencies are **not** supported.
|
||
|
||
### `u.renderer`
|
||
|
||
The renderer library renders hyperscript into a buffer. Each render performs a
|
||
minimal set of changes in order to transform the current buffer text into the
|
||
desired state.
|
||
|
||
**Hyperscript** is just 1) _text_ 2) `<text>` tags, which can be nested in 3)
|
||
Lua tables for readability:
|
||
|
||
```lua
|
||
local h = require('u.renderer').h
|
||
-- Hyperscript can be organized into tables:
|
||
{
|
||
"Hello, ",
|
||
{
|
||
"I am ", { "a" }, " nested table.",
|
||
},
|
||
'\n', -- newlines must be explicitly specified
|
||
|
||
-- booleans/nil are ignored:
|
||
some_conditional_flag and 'This text only shows when the flag is true',
|
||
-- e.g., use the above to show newlines in lists:
|
||
idx > 1 and '\n',
|
||
|
||
-- <text> tags are specified like so:
|
||
-- h('text', attributes, children)
|
||
h('text', {}, "I am a text node."),
|
||
|
||
-- <text> tags can be highlighted:
|
||
h('text', { hl = 'Comment' }, "I am highlighted."),
|
||
|
||
-- <text> tags can respond to key events:
|
||
h('text', {
|
||
hl = 'Keyword',
|
||
nmap = {
|
||
["<CR>"] = function()
|
||
print("Hello World")
|
||
-- Return '' to swallow the event:
|
||
return ''
|
||
end,
|
||
},
|
||
}, "I am a text node."),
|
||
}
|
||
```
|
||
|
||
Managing complex tables of hyperscript can be done more ergonomically using the
|
||
`TreeBuilder` helper class:
|
||
|
||
```lua
|
||
local TreeBuilder = require('u.renderer').TreeBuilder
|
||
|
||
-- ...
|
||
renderer:render(
|
||
TreeBuilder.new()
|
||
-- text:
|
||
:put('some text')
|
||
-- hyperscript tables:
|
||
:put({ 'some text', 'more hyperscript' })
|
||
-- hyperscript tags:
|
||
:put_h('text', { --[[attributes]] }, { --[[children]] })
|
||
-- callbacks:
|
||
--- @param tb TreeBuilder
|
||
:nest(function(tb)
|
||
tb:put('some text')
|
||
end)
|
||
:tree()
|
||
)
|
||
```
|
||
|
||
**Rendering**: The renderer library provides a `render` function that takes
|
||
hyperscript in, and converts it to formatted buffer text:
|
||
|
||
```lua
|
||
local Renderer = require('u.renderer').Renderer
|
||
local renderer = Renderer:new(0 --[[buffer number]])
|
||
renderer:render {
|
||
-- ...hyperscript...
|
||
}
|
||
|
||
-- or, if you already have a buffer:
|
||
local Buffer = require('u.buffer')
|
||
local buf = Buffer.current()
|
||
buf:render {
|
||
-- ...hyperscript...
|
||
}
|
||
```
|
||
|
||
## Range Usage
|
||
|
||
### A note on indices
|
||
|
||
<blockquote>
|
||
<del>
|
||
I love NeoVim. I am coming to love Lua. I don't like 1-based indices; perhaps I
|
||
am too old. Perhaps I am too steeped in the history of loving the elegance of
|
||
simple pointer arithmetic. Regardless, the way positions are addressed in
|
||
NeoVim/Vim is (terrifyingly) mixed. Some methods return 1-based, others accept
|
||
only 0-based. In order to stay sane, I had to make a choice to store everything
|
||
in one, uniform representation in this library. I chose (what I humbly think is
|
||
the only sane way) to stick with the tried-and-true 0-based index scheme. That
|
||
abstraction leaks into the public API of this library.
|
||
</del>
|
||
</blockquote>
|
||
|
||
<br />
|
||
<b>This has changed in v2</b>. After much thought, I realized that:
|
||
|
||
1. The 0-based indexing in NeoVim is prevelant in the `:api`, which is designed
|
||
to be exposed to many languages. As such, it makes sense for this interface
|
||
to use 0-based indexing. However, many internal Vim functions use 1-based
|
||
indexing.
|
||
2. This is a Lua library (surprise, surprise, duh) - the idioms of the language
|
||
should take precedence over my preference
|
||
3. There were subtle bugs in the code where indices weren't being normalized to
|
||
0-based, anyways. Somehow it worked most of the time.
|
||
|
||
As such, this library now uses 1-based indexing everywhere, doing the necessary
|
||
interop conversions when calling `:api` functions.
|
||
|
||
### 1. Creating a Range
|
||
|
||
The `Range` utility is the main feature upon which most other things in this
|
||
library are built, aside from a few standalone utilities. Ranges can be
|
||
constructed manually, or preferably, obtained based on a variety of contexts.
|
||
|
||
```lua
|
||
local Range = require 'u.range'
|
||
local start = Pos.new(0, 1, 1) -- Line 1, first column
|
||
local stop = Pos.new(0, 3, 1) -- Line 3, first column
|
||
|
||
Range.new(start, stop, 'v') -- charwise selection
|
||
Range.new(start, stop, 'V') -- linewise selection
|
||
```
|
||
|
||
This is usually not how you want to obtain a `Range`, however. Usually you want
|
||
to get the corresponding context of an edit operation and just "get me the
|
||
current Range that represents this context".
|
||
|
||
```lua
|
||
-- get the first line in a buffer:
|
||
Range.from_line(bufnr, 1)
|
||
|
||
-- Text Objects (any text object valid in your configuration is supported):
|
||
-- get the word the cursor is on:
|
||
Range.from_motion('iw')
|
||
-- get the WORD the cursor is on:
|
||
Range.from_motion('iW')
|
||
-- get the "..." the cursor is within:
|
||
Range.from_motion('a"')
|
||
|
||
-- Get the currently visually selected text:
|
||
-- NOTE: this does NOT work within certain contexts; more specialized utilities
|
||
-- are more appropriate in certain circumstances
|
||
Range.from_vtext()
|
||
|
||
--
|
||
-- Get the operated on text obtained from a motion:
|
||
-- (HINT: use the opkeymap utility to make this less verbose)
|
||
--
|
||
--- @param ty 'char'|'line'|'block'
|
||
function MyOpFunc(ty)
|
||
local range = Range.from_op_func(ty)
|
||
-- do something with the range
|
||
end
|
||
-- Try invoking this with: `<Leader>toaw`, and the current word will be the
|
||
-- context:
|
||
vim.keymap.set('<Leader>to', function()
|
||
vim.g.operatorfunc = 'v:lua.MyOpFunc'
|
||
return 'g@'
|
||
end, { expr = true })
|
||
|
||
--
|
||
-- Commands:
|
||
--
|
||
-- When executing commands in a visual context, getting the selected text has
|
||
-- to be done differently:
|
||
vim.api.nvim_create_user_command('MyCmd', function(args)
|
||
local range = Range.from_cmd_args(args)
|
||
if range == nil then
|
||
-- the command was executed in normal mode
|
||
else
|
||
-- ...
|
||
end
|
||
end, { range = true })
|
||
```
|
||
|
||
So far, that's a lot of ways to _get_ a `Range`. But what can you do with a
|
||
range once you have one? Plenty, it turns out!
|
||
|
||
```lua
|
||
local range = ...
|
||
range:lines() -- get the lines in the range's region
|
||
range:text() -- get the text (i.e., string) in the range's region
|
||
range:line(1) -- get the first line within this range
|
||
range:line(-1) -- get the last line within this range
|
||
-- replace with new contents:
|
||
range:replace {
|
||
'replacement line 1',
|
||
'replacement line 2',
|
||
}
|
||
range:replace 'with a string'
|
||
-- delete the contents of the range:
|
||
range:replace(nil)
|
||
```
|
||
|
||
### 2. Defining Key Mappings over Motions
|
||
|
||
Define custom (dot-repeatable) key mappings for text objects:
|
||
|
||
```lua
|
||
local opkeymap = require 'u.opkeymap'
|
||
|
||
-- invoke this function by typing, for example, `<leader>riw`:
|
||
-- `range` will contain the bounds of the motion `iw`.
|
||
opkeymap('n', '<leader>r', function(range)
|
||
print(range:text()) -- Prints the text within the selected range
|
||
end)
|
||
```
|
||
|
||
### 3. Working with Code Writer
|
||
|
||
To write code with indentation, use the `CodeWriter` class:
|
||
|
||
```lua
|
||
local CodeWriter = require 'u.codewriter'
|
||
local cw = CodeWriter.new()
|
||
cw:write('{')
|
||
cw:indent(function(innerCW)
|
||
innerCW:write('x: 123')
|
||
end)
|
||
cw:write('}')
|
||
```
|
||
|
||
### 4. Utility Functions
|
||
|
||
#### Custom Text Objects
|
||
|
||
Simply by returning a `Range` or a `Pos`, you can easily and quickly define
|
||
your own text objects:
|
||
|
||
```lua
|
||
local txtobj = require 'u.txtobj'
|
||
local Range = require 'u.range'
|
||
|
||
-- Select whole file:
|
||
txtobj.define('ag', function()
|
||
return Range.from_buf_text()
|
||
end)
|
||
```
|
||
|
||
#### Buffer Management
|
||
|
||
Access and manipulate buffers easily:
|
||
|
||
```lua
|
||
local Buffer = require 'u.buffer'
|
||
local buf = Buffer.current()
|
||
buf:line_count() -- the number of lines in the current buffer
|
||
buf:get_option '...'
|
||
buf:set_option('...', ...)
|
||
buf:get_var '...'
|
||
buf:set_var('...', ...)
|
||
buf:all() -- returns a Range representing the entire buffer
|
||
buf:is_empty() -- returns true if the buffer has no text
|
||
buf:append_line '...'
|
||
buf:line(1) -- returns a Range representing the first line in the buffer
|
||
buf:line(-1) -- returns a Range representing the last line in the buffer
|
||
buf:lines(1, 2) -- returns a Range representing the first two lines in the buffer
|
||
buf:lines(2, -2) -- returns a Range representing all but the first and last lines of a buffer
|
||
buf:txtobj('iw') -- returns a Range representing the text object 'iw' in the give buffer
|
||
```
|
||
|
||
## License (MIT)
|
||
|
||
Copyright (c) 2024 jrapodaca@gmail.com
|
||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||
this software and associated documentation files (the "Software"), to deal in
|
||
the Software without restriction, including without limitation the rights to
|
||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||
of the Software, and to permit persons to whom the Software is furnished to do
|
||
so, subject to the following conditions:
|
||
|
||
The above copyright notice and this permission notice shall be included in all
|
||
copies or substantial portions of the Software.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
SOFTWARE.
|