update README
Some checks failed
ci / ci (push) Failing after 3m6s

This commit is contained in:
2026-04-06 17:34:07 -06:00
parent 6061630492
commit 43837ac33a

412
README.md
View File

@@ -1,40 +1,30 @@
# u.nvim # u.nvim
Welcome to **u.nvim** - a powerful Lua library designed to enhance your text Welcome to **u.nvim** - a Lua library for text manipulation in Neovim, focusing on
manipulation experience in NeoVim, focusing on text-manipulation utilities. range-based text operations, positions, and operator-pending mappings.
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` This is a **single-file micro-library** meant to be vendored in your plugin or
does nothing. It is meant to be used by plugin authors, to make their lives config. On its own, `u.nvim` does nothing. It is meant to be used by plugin
easier based on the variety of utilities I found I needed while growing my authors to make their lives easier. To get an idea of what a plugin built on
NeoVim config. To get an idea of what a plugin built on top of `u.nvim` would top of `u.nvim` would look like, check out the [examples/](./examples/) directory.
look like, check out the [examples/](./examples/) directory.
## Features ## Features
- **Rendering System**: a utility that can declaratively render NeoVim-specific - **Range Utility**: Get context-aware selections with ease. Replace regions with
hyperscript into a buffer, supporting creating/managing extmarks, highlights, new text. Think of it as a programmatic way to work with visual selections.
and key-event handling (requires NeoVim >0.11) - **Position Utilities**: Work with cursor positions, marks, and extmarks.
- **Signals**: a simple dependency tracking system that pairs well with the - **Operator Key Mapping**: Flexible key mapping that works with motions.
rendering utilities for creating reactive/interactive UIs in NeoVim. - **Text Object Definitions**: Define custom text objects easily.
- **Range Utility**: Get context-aware selections with ease. Replace regions - **User Command Helpers**: Create commands with range support.
with new text. Think of it as a programmatic way to work with visual - **Repeat Utilities**: Dot-repeat support for custom operations.
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 ### Installation
This being a library, and not a proper plugin, it is recommended that you This being a library, and not a proper plugin, it is recommended that you vendor
vendor the specific version of this library that you need, including it in your the specific version of this library that you need, including it in your code.
code. Package managers are a developing landscape for Lua in the context of Package managers are a developing landscape for Lua in the context of NeoVim.
NeoVim. Perhaps in the future, `lux` will eliminate the need to vendor this Perhaps in the future, `lux` will eliminate the need to vendor this library in
library in your application code. your application code.
#### If you are a Plugin Author #### If you are a Plugin Author
@@ -98,201 +88,6 @@ vim.pack.add { 'https://github.com/jrop/u.nvim' }
</details> </details>
## 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 ## Range Usage
### A note on indices ### A note on indices
@@ -327,17 +122,17 @@ interop conversions when calling `:api` functions.
### 1. Creating a Range ### 1. Creating a Range
The `Range` utility is the main feature upon which most other things in this The `Range` utility is the main feature of this library. Ranges can be constructed
library are built, aside from a few standalone utilities. Ranges can be manually, or preferably, obtained based on a variety of contexts.
constructed manually, or preferably, obtained based on a variety of contexts.
```lua ```lua
local Range = require 'u.range' local u = require 'u'
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 local start = u.Pos.new(nil, 1, 1) -- Line 1, first column
Range.new(start, stop, 'V') -- linewise selection local stop = u.Pos.new(nil, 3, 1) -- Line 3, first column
u.Range.new(start, stop, 'v') -- charwise selection
u.Range.new(start, stop, 'V') -- linewise selection
``` ```
This is usually not how you want to obtain a `Range`, however. Usually you want This is usually not how you want to obtain a `Range`, however. Usually you want
@@ -345,21 +140,23 @@ to get the corresponding context of an edit operation and just "get me the
current Range that represents this context". current Range that represents this context".
```lua ```lua
local u = require 'u'
-- get the first line in a buffer: -- get the first line in a buffer:
Range.from_line(bufnr, 1) u.Range.from_line(bufnr, 1)
-- Text Objects (any text object valid in your configuration is supported): -- Text Objects (any text object valid in your configuration is supported):
-- get the word the cursor is on: -- get the word the cursor is on:
Range.from_motion('iw') u.Range.from_motion('iw')
-- get the WORD the cursor is on: -- get the WORD the cursor is on:
Range.from_motion('iW') u.Range.from_motion('iW')
-- get the "..." the cursor is within: -- get the "..." the cursor is within:
Range.from_motion('a"') u.Range.from_motion('a"')
-- Get the currently visually selected text: -- Get the currently visually selected text:
-- NOTE: this does NOT work within certain contexts; more specialized utilities -- NOTE: this does NOT work within certain contexts; more specialized utilities
-- are more appropriate in certain circumstances -- are more appropriate in certain circumstances
Range.from_vtext() u.Range.from_vtext()
-- --
-- Get the operated on text obtained from a motion: -- Get the operated on text obtained from a motion:
@@ -367,7 +164,7 @@ Range.from_vtext()
-- --
--- @param ty 'char'|'line'|'block' --- @param ty 'char'|'line'|'block'
function MyOpFunc(ty) function MyOpFunc(ty)
local range = Range.from_op_func(ty) local range = u.Range.from_op_func(ty)
-- do something with the range -- do something with the range
end end
-- Try invoking this with: `<Leader>toaw`, and the current word will be the -- Try invoking this with: `<Leader>toaw`, and the current word will be the
@@ -383,7 +180,7 @@ end, { expr = true })
-- When executing commands in a visual context, getting the selected text has -- When executing commands in a visual context, getting the selected text has
-- to be done differently: -- to be done differently:
vim.api.nvim_create_user_command('MyCmd', function(args) vim.api.nvim_create_user_command('MyCmd', function(args)
local range = Range.from_cmd_args(args) local range = u.Range.from_cmd_args(args)
if range == nil then if range == nil then
-- the command was executed in normal mode -- the command was executed in normal mode
else else
@@ -398,7 +195,7 @@ range once you have one? Plenty, it turns out!
```lua ```lua
local range = ... local range = ...
range:lines() -- get the lines in the range's region range:lines() -- get the lines in the range's region
range:text() -- get the text (i.e., string) 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 first line within this range
range:line(-1) -- get the last line within this range range:line(-1) -- get the last line within this range
-- replace with new contents: -- replace with new contents:
@@ -416,68 +213,125 @@ range:replace(nil)
Define custom (dot-repeatable) key mappings for text objects: Define custom (dot-repeatable) key mappings for text objects:
```lua ```lua
local opkeymap = require 'u.opkeymap' local u = require 'u'
-- invoke this function by typing, for example, `<leader>riw`: -- invoke this function by typing, for example, `<leader>riw`:
-- `range` will contain the bounds of the motion `iw`. -- `range` will contain the bounds of the motion `iw`.
opkeymap('n', '<leader>r', function(range) u.opkeymap('n', '<leader>r', function(range)
print(range:text()) -- Prints the text within the selected range print(range:text()) -- Prints the text within the selected range
end) end)
``` ```
### 3. Working with Code Writer ### 3. Utility Functions
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 #### Custom Text Objects
Simply by returning a `Range` or a `Pos`, you can easily and quickly define Simply by returning a `Range`, you can easily define your own text objects:
your own text objects:
```lua ```lua
local txtobj = require 'u.txtobj' local u = require 'u'
local Range = require 'u.range'
-- Select whole file: -- Select whole file:
txtobj.define('ag', function() u.define_txtobj('ag', function()
return Range.from_buf_text() return u.Range.from_buf_text()
end)
-- Select content inside nearest quotes:
u.define_txtobj('iq', function()
return u.Range.find_nearest_quotes()
end)
-- Select content inside nearest brackets:
u.define_txtobj('ib', function()
return u.Range.find_nearest_brackets()
end) end)
``` ```
#### Buffer Management #### User Commands with Range Support
Access and manipulate buffers easily: Create user commands that work with visual selections:
```lua ```lua
local Buffer = require 'u.buffer' local u = require 'u'
local buf = Buffer.current()
buf.b.<option> -- get buffer-local variables u.ucmd('MyCmd', function(args)
buf.b.<option> = ... -- set buffer-local variables if args.info then
buf.bo.<option> -- get buffer options -- args.info is a Range representing the selection
buf.bo.<option> = ... -- set buffer options print('Selected text:', args.info:text())
buf:line_count() -- the number of lines in the current buffer else
buf:all() -- returns a Range representing the entire buffer -- No range provided
buf:is_empty() -- returns true if the buffer has no text print('No selection')
buf:append_line '...' end
buf:line(1) -- returns a Range representing the first line in the buffer end, { range = true })
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
``` ```
#### Repeat Utilities
Enable dot-repeat for custom operations:
```lua
local u = require 'u'
-- Call this in your plugin's setup:
u.repeat_.setup()
-- Then use in your operations:
u.repeat_.run_repeatable(function()
-- Your mutation here
-- This will be repeatable with '.'
end)
```
## API Reference
### `u.Pos`
Position class representing a location in a buffer.
```lua
local pos = u.Pos.new(bufnr, lnum, col, off)
pos:next(1) -- next position (forward)
pos:next(-1) -- previous position (backward)
pos:char() -- character at position
pos:line() -- line text
pos:eol() -- end of line position
pos:save_to_cursor() -- move cursor to position
```
### `u.Range`
Range class representing a text region.
```lua
local range = u.Range.new(start, stop, mode)
range:text() -- get text content
range:lines() -- get lines as array
range:replace(text) -- replace with new text
range:contains(pos) -- check if position is within range
range:shrink(n) -- shrink range by n characters from each side
range:highlight('Search') -- highlight range temporarily
```
### `u.opkeymap(mode, lhs, rhs, opts)`
Create operator-pending keymaps.
### `u.define_txtobj(key_seq, fn, opts)`
Define custom text objects.
### `u.ucmd(name, cmd, opts)`
Create user commands with enhanced range support.
### `u.create_delegated_cmd_args(args)`
Create arguments for delegating between commands.
### `u.repeat_`
Module for dot-repeat support.
## License (MIT) ## License (MIT)
Copyright (c) 2024 jrapodaca@gmail.com Copyright (c) 2024 jrapodaca@gmail.com