diff --git a/README.md b/README.md index 9999446..6f8822b 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,30 @@ # 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. +Welcome to **u.nvim** - a Lua library for text manipulation in Neovim, focusing on +range-based text operations, positions, and operator-pending mappings. -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. +This is a **single-file micro-library** meant to be vendored in your plugin or +config. On its own, `u.nvim` does nothing. It is meant to be used by plugin +authors to make their lives easier. 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. +- **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. +- **Position Utilities**: Work with cursor positions, marks, and extmarks. +- **Operator Key Mapping**: Flexible key mapping that works with motions. +- **Text Object Definitions**: Define custom text objects easily. +- **User Command Helpers**: Create commands with range support. +- **Repeat Utilities**: Dot-repeat support for custom operations. ### 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. +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. #### If you are a Plugin Author @@ -98,201 +88,6 @@ vim.pack.add { 'https://github.com/jrop/u.nvim' } -## 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 @@ -327,17 +122,17 @@ 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 of this library. 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 +local u = require 'u' -Range.new(start, stop, 'v') -- charwise selection -Range.new(start, stop, 'V') -- linewise selection +local start = u.Pos.new(nil, 1, 1) -- Line 1, first column +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 @@ -345,21 +140,23 @@ to get the corresponding context of an edit operation and just "get me the current Range that represents this context". ```lua +local u = require 'u' + -- 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): -- get the word the cursor is on: -Range.from_motion('iw') +u.Range.from_motion('iw') -- get the WORD the cursor is on: -Range.from_motion('iW') +u.Range.from_motion('iW') -- get the "..." the cursor is within: -Range.from_motion('a"') +u.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() +u.Range.from_vtext() -- -- Get the operated on text obtained from a motion: @@ -367,7 +164,7 @@ Range.from_vtext() -- --- @param ty 'char'|'line'|'block' function MyOpFunc(ty) - local range = Range.from_op_func(ty) + local range = u.Range.from_op_func(ty) -- do something with the range end -- Try invoking this with: `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 -- to be done differently: 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 -- the command was executed in normal mode else @@ -398,7 +195,7 @@ 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: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: @@ -416,68 +213,125 @@ range:replace(nil) Define custom (dot-repeatable) key mappings for text objects: ```lua -local opkeymap = require 'u.opkeymap' +local u = require 'u' -- invoke this function by typing, for example, `riw`: -- `range` will contain the bounds of the motion `iw`. -opkeymap('n', 'r', function(range) +u.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 +### 3. Utility Functions #### 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`, you can easily define your own text objects: ```lua -local txtobj = require 'u.txtobj' -local Range = require 'u.range' +local u = require 'u' -- Select whole file: -txtobj.define('ag', function() - return Range.from_buf_text() +u.define_txtobj('ag', function() + 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) ``` -#### Buffer Management +#### User Commands with Range Support -Access and manipulate buffers easily: +Create user commands that work with visual selections: ```lua -local Buffer = require 'u.buffer' -local buf = Buffer.current() -buf.b.