update lua API to 1-based indices; add renderer
All checks were successful
NeoVim tests / plenary-tests (push) Successful in 11s
All checks were successful
NeoVim tests / plenary-tests (push) Successful in 11s
This commit is contained in:
350
README.md
350
README.md
@@ -1,76 +1,315 @@
|
||||
# u.nvim
|
||||
|
||||
🚨🚨 **BRANCH NOTICE: further development is happening on the `v2` branch. In the future, `v2` will be merged into `master`. If you want to pin to an older version of this library, please refer to a specific commit, or the `v1` branch.** 🚨🚨
|
||||
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.
|
||||
|
||||
🚨🚨[CLICK HERE FOR v2](https://github.com/jrop/u.nvim/tree/v2)🚨🚨
|
||||
|
||||
Welcome to **u.nvim** – a powerful Lua library designed to enhance your text manipulation experience in NeoVim, focusing primarily on a context-aware "Range" utility. This utility allows you to work efficiently with text selections based on various conditions, in a variety of contexts, 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.
|
||||
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
|
||||
|
||||
- **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).
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
lazy.nvim:
|
||||
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
|
||||
-- Setting `lazy = true` ensures that the library is only loaded
|
||||
-- when `require 'u.<utility>' is called.
|
||||
{ 'jrop/u.nvim', lazy = true }
|
||||
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)
|
||||
```
|
||||
|
||||
## Usage
|
||||
</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
|
||||
|
||||
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.
|
||||
<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.
|
||||
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, 0, 0) -- Line 1, first column
|
||||
local stop = Pos.new(0, 2, 0) -- Line 3, first column
|
||||
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".
|
||||
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(0, 0)
|
||||
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_text_object('iw')
|
||||
Range.from_motion('iw')
|
||||
-- get the WORD the cursor is on:
|
||||
Range.from_text_object('iW')
|
||||
Range.from_motion('iW')
|
||||
-- get the "..." the cursor is within:
|
||||
Range.from_text_object('a"')
|
||||
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
|
||||
-- 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'
|
||||
--- @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:
|
||||
-- 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@'
|
||||
@@ -79,7 +318,8 @@ end, { expr = true })
|
||||
--
|
||||
-- Commands:
|
||||
--
|
||||
-- When executing commands in a visual context, getting the selected text has to be done differently:
|
||||
-- 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
|
||||
@@ -90,14 +330,15 @@ vim.api.nvim_create_user_command('MyCmd', function(args)
|
||||
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!
|
||||
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:line0(0) -- get the first line within this range
|
||||
range:line0(-1) -- get the last line within this range
|
||||
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',
|
||||
@@ -140,14 +381,15 @@ cw:write('}')
|
||||
|
||||
#### Custom Text Objects
|
||||
|
||||
Simply by returning a `Range` or a `Pos`, you can easily and quickly define your own text objects:
|
||||
Simply by returning a `Range` or a `Pos`, you can easily and quickly define
|
||||
your own text objects:
|
||||
|
||||
```lua
|
||||
local utils = require 'u.utils'
|
||||
local txtobj = require 'u.txtobj'
|
||||
local Range = require 'u.range'
|
||||
|
||||
-- Select whole file:
|
||||
utils.define_text_object('ag', function()
|
||||
txtobj.define('ag', function()
|
||||
return Range.from_buf_text()
|
||||
end)
|
||||
```
|
||||
@@ -159,27 +401,39 @@ 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.b.<option> -- get buffer-local variables
|
||||
buf.b.<option> = ... -- set buffer-local variables
|
||||
buf.bo.<option> -- get buffer options
|
||||
buf.bo.<option> = ... -- set buffer options
|
||||
buf:line_count() -- the number of lines in the current buffer
|
||||
buf:all() -- returns a Range representing the entire buffer
|
||||
buf:is_empty() -- returns true if the buffer has no text
|
||||
buf:append_line '...'
|
||||
buf:line0(0) -- returns a Range representing the first line in the buffer
|
||||
buf:line0(-1) -- returns a Range representing the last line in the buffer
|
||||
buf:lines(0, 1) -- returns a Range representing the first two lines in the buffer
|
||||
buf:lines(1, -2) -- returns a Range representing all but the first and last lines of a buffer
|
||||
buf:text_object('iw') -- returns a Range representing the text object 'iw' in the give buffer
|
||||
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:
|
||||
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 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.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user