# 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:
Example Code: counter.lua
```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 = {
[''] = 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 = {
[''] = 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 on each "button" above to increment/decrement the counter.' },
}
end)
```
### `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) `` 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',
-- tags are specified like so:
-- h('text', attributes, children)
h('text', {}, "I am a text node."),
-- tags can be highlighted:
h('text', { hl = 'Comment' }, "I am highlighted."),
-- tags can respond to key events:
h('text', {
hl = 'Keyword',
nmap = {
[""] = 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
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.
This has changed in v2. 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: `toaw`, and the current word will be the
-- context:
vim.keymap.set('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, `riw`:
-- `range` will contain the bounds of the motion `iw`.
opkeymap('n', '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.b.