Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9ea5b0658 | |||
| 88b7a11efa | |||
| b473ac3923 | |||
| 63c920dbf1 | |||
| 12945a4cdf | |||
| 6f86bfaa42 | |||
| 81ba1bb96b | |||
| 35b6e123ac | |||
| 237bc9ba5e | |||
| c760c495b7 |
12
.busted
Normal file
12
.busted
Normal file
@@ -0,0 +1,12 @@
|
||||
return {
|
||||
_all = {
|
||||
lpath = "lua/?.lua;lua/?/init.lua",
|
||||
lua = "nvim -u NONE -i NONE -l",
|
||||
},
|
||||
default = {
|
||||
verbose = true
|
||||
},
|
||||
tests = {
|
||||
verbose = true
|
||||
},
|
||||
}
|
||||
22
.emmyrc.json
Normal file
22
.emmyrc.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
|
||||
"diagnostics": {
|
||||
"disable": [
|
||||
"access-invisible",
|
||||
"redefined-local"
|
||||
]
|
||||
},
|
||||
"runtime": {
|
||||
"version": "LuaJIT"
|
||||
},
|
||||
"workspace": {
|
||||
"ignoreDir": [
|
||||
".prefix"
|
||||
],
|
||||
"library": [
|
||||
"$VIMRUNTIME",
|
||||
"library/busted",
|
||||
"library/luv"
|
||||
]
|
||||
}
|
||||
}
|
||||
63
.github/workflows/ci.yaml
vendored
63
.github/workflows/ci.yaml
vendored
@@ -1,15 +1,60 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: NeoVim tests
|
||||
on: [push]
|
||||
name: ci
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
tags: ["*"]
|
||||
branches: ["*"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
MISE_EXPERIMENTAL: true
|
||||
|
||||
jobs:
|
||||
plenary-tests:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rhysd/action-setup-vim@v1
|
||||
with:
|
||||
neovim: true
|
||||
version: v0.10.1
|
||||
- run: make test
|
||||
submodules: true
|
||||
|
||||
- name: Install apt dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libreadline-dev
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
if [ -n "${{secrets.TOKEN}}" ]; then
|
||||
export GITHUB_TOKEN="${{secrets.TOKEN}}"
|
||||
fi
|
||||
export MISE_GITHUB_TOKEN="$GITHUB_TOKEN"
|
||||
echo "$GITHUB_TOKEN" >> $GITHUB_ENV
|
||||
echo "$MISE_GITHUB_TOKEN" >> $GITHUB_ENV
|
||||
|
||||
- name: Install mise
|
||||
run: |
|
||||
curl https://mise.run | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH
|
||||
echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH
|
||||
|
||||
- name: Install mise dependencies
|
||||
run: |
|
||||
mise install
|
||||
mise list --local
|
||||
|
||||
- name: Check Lua formatting
|
||||
run: mise run fmt:check
|
||||
|
||||
- name: Check for type-errors
|
||||
run: mise run lint
|
||||
|
||||
- name: Run tests
|
||||
run: mise run test:all
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
.prefix/
|
||||
*.src.rock
|
||||
*.aider*
|
||||
luacov.*.out
|
||||
|
||||
8
.gitmodules
vendored
Normal file
8
.gitmodules
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
[submodule "library/busted"]
|
||||
path = library/busted
|
||||
url = https://github.com/LuaCATS/busted
|
||||
branch = main
|
||||
[submodule "library/luv"]
|
||||
path = library/luv
|
||||
url = https://github.com/LuaCATS/luv
|
||||
branch = main
|
||||
1
.styluaignore
Normal file
1
.styluaignore
Normal file
@@ -0,0 +1 @@
|
||||
library/
|
||||
34
AGENTS.md
Normal file
34
AGENTS.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
|
||||
Single-file Neovim Lua micro-library (`lua/u.lua`) for range-based text operations, positions, operator-pending mappings, and text objects. Not a plugin — meant to be vendored by other plugins.
|
||||
|
||||
## Commands
|
||||
|
||||
All tasks use `mise`:
|
||||
|
||||
- `mise run fmt` — format (stylua)
|
||||
- `mise run fmt:check` — check formatting
|
||||
- `mise run lint` — type-check with `emmylua_check` (ignores `.prefix/`)
|
||||
- `mise run test` — run busted against Neovim 0.12.1 (includes `test:prepare`)
|
||||
- `mise run test:all` — test against 0.11.5, 0.12.1, and nightly
|
||||
- `mise run test:coverage` — test with luacov (only covers `lua/u$`)
|
||||
- `mise run ci` — `fmt:check` → `lint` → `test:all`
|
||||
|
||||
Run a single spec file: `busted spec/u_spec.lua` (after `mise run test:prepare`).
|
||||
|
||||
## Architecture
|
||||
|
||||
- **`lua/u.lua`** — the entire library (~1300 lines): `Pos`, `Range`, `opkeymap`, `define_txtobj`, `ucmd`, `repeat_`
|
||||
- **`spec/u_spec.lua`** — all tests in one file
|
||||
- **`library/`** — type stubs for busted/luv (stylua-ignored, used by emmylua)
|
||||
- **`.prefix/`** — neovim version installs managed by `nvimv` (git-ignored, emmylua-ignored)
|
||||
- **`examples/`** — usage examples (surround, splitjoin, text-objects, matcher)
|
||||
|
||||
## Conventions
|
||||
|
||||
- **1-based indexing** everywhere (v2+). `Pos.from00()` / `Range.from00()` convert from 0-based Neovim API values.
|
||||
- Stylua: LuaJIT syntax, single quotes, no call parens, 2-space indent, sort requires, 100 col width
|
||||
- Tests run inside Neovim via busted's `lua = "nvim -u NONE -i NONE -l"` (set in `.busted`)
|
||||
- `test:prepare` installs busted rocks via `luarocks test --prepare` and manages nightly Neovim via `nvimv`
|
||||
15
Makefile
15
Makefile
@@ -1,15 +0,0 @@
|
||||
PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim
|
||||
|
||||
all: lint test
|
||||
|
||||
lint:
|
||||
selene .
|
||||
|
||||
fmt:
|
||||
stylua .
|
||||
|
||||
test: $(PLENARY_DIR)
|
||||
NVIM_APPNAME=noplugstest nvim -u NORC --headless -c 'set packpath+=~/.local/share/nvim/site' -c 'packadd plenary.nvim' -c "PlenaryBustedDirectory spec/"
|
||||
|
||||
$(PLENARY_DIR):
|
||||
git clone https://github.com/nvim-lua/plenary.nvim/ $(PLENARY_DIR)
|
||||
332
README.md
332
README.md
@@ -1,76 +1,174 @@
|
||||
# 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 Lua library for text manipulation in Neovim, focusing on
|
||||
range-based text operations, positions, and operator-pending mappings.
|
||||
|
||||
🚨🚨[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 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
|
||||
|
||||
- **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
|
||||
|
||||
lazy.nvim:
|
||||
```lua
|
||||
-- Setting `lazy = true` ensures that the library is only loaded
|
||||
-- when `require 'u.<utility>' is called.
|
||||
{ 'jrop/u.nvim', lazy = true }
|
||||
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
|
||||
|
||||
Neovim does not have a good answer for automatic management of plugin dependencies. As such, it is recommended that library authors vendor u.nvim within their plugin. **u.nvim** is implemented in a single file, so this should be relatively painless. Furthermore, `lua/u.lua` versions are published into artifact tags `artifact-vX.Y.Z` as `init.lua` so that plugin authors can add u.nvim as a submodule to their plugin.
|
||||
|
||||
<details>
|
||||
<summary>Example git submodule setup</summary>
|
||||
|
||||
```bash
|
||||
# In your plugin repository
|
||||
git submodule add -- https://github.com/jrop/u.nvim lua/my_plugin/u
|
||||
cd lua/my_plugin/u/
|
||||
git checkout artifact-v0.2.0 # put whatever version of u.nvim you want to pin here
|
||||
# ... commit the submodule within your repo
|
||||
|
||||
# This would place u.nvim@v0.2.0 at:
|
||||
# lua/my_plugin/u/init.lua
|
||||
```
|
||||
|
||||
## Usage
|
||||
Then in your plugin code:
|
||||
|
||||
```lua
|
||||
local u = require('my_plugin.u')
|
||||
|
||||
local Pos = u.Pos
|
||||
local Range = u.Range
|
||||
-- etc.
|
||||
```
|
||||
|
||||
This approach allows plugin authors to:
|
||||
- Pin to specific versions of u.nvim
|
||||
- Get updates by pulling/committing new **u.nvim** versions (i.e., the usual git submodule way)
|
||||
- Keep the dependency explicit and version-controlled
|
||||
- Avoid namespace conflicts with user-installed plugins
|
||||
|
||||
</details>
|
||||
|
||||
#### If you are a User
|
||||
|
||||
If you want to use u.nvim in your config directly:
|
||||
|
||||
<details>
|
||||
<summary>vim.pack</summary>
|
||||
|
||||
```lua
|
||||
vim.pack.add { 'https://github.com/jrop/u.nvim' }
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>lazy.nvim</summary>
|
||||
|
||||
```lua
|
||||
{
|
||||
'jrop/u.nvim',
|
||||
lazy = true,
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 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 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, 0, 0) -- Line 1, first column
|
||||
local stop = Pos.new(0, 2, 0) -- 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 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
|
||||
local u = require 'u'
|
||||
|
||||
-- get the first line in a buffer:
|
||||
Range.from_line(0, 0)
|
||||
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_text_object('iw')
|
||||
u.Range.from_motion('iw')
|
||||
-- get the WORD the cursor is on:
|
||||
Range.from_text_object('iW')
|
||||
u.Range.from_motion('iW')
|
||||
-- get the "..." the cursor is within:
|
||||
Range.from_text_object('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()
|
||||
-- NOTE: this does NOT work within certain contexts; more specialized utilities
|
||||
-- are more appropriate in certain circumstances
|
||||
u.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)
|
||||
local range = u.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,9 +177,10 @@ 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)
|
||||
local range = u.Range.from_cmd_args(args)
|
||||
if range == nil then
|
||||
-- the command was executed in normal mode
|
||||
else
|
||||
@@ -90,14 +189,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: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',
|
||||
@@ -113,73 +213,143 @@ 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, `<leader>riw`:
|
||||
-- `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
|
||||
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 utils = require 'u.utils'
|
||||
local Range = require 'u.range'
|
||||
local u = require 'u'
|
||||
|
||||
-- Select whole file:
|
||||
utils.define_text_object('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: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: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
|
||||
local u = require 'u'
|
||||
|
||||
u.ucmd('MyCmd', function(args)
|
||||
if args.info then
|
||||
-- args.info is a Range representing the selection
|
||||
print('Selected text:', args.info:text())
|
||||
else
|
||||
-- No range provided
|
||||
print('No selection')
|
||||
end
|
||||
end, { range = true })
|
||||
```
|
||||
|
||||
#### 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)
|
||||
|
||||
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.
|
||||
|
||||
51
examples/matcher.lua
Normal file
51
examples/matcher.lua
Normal file
@@ -0,0 +1,51 @@
|
||||
--
|
||||
-- Bracket matcher: highlights the nearest matching pair of brackets (like MatchParen)
|
||||
-- when the cursor is near them. Updates dynamically on CursorMoved in normal mode.
|
||||
--
|
||||
local u = require 'u'
|
||||
local Range = u.Range
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @type { clear: fun() }[]
|
||||
local HIGHLIGHTS = {}
|
||||
local LAST_RANGE = nil
|
||||
|
||||
local function clear_highlights()
|
||||
for _, hl in ipairs(HIGHLIGHTS) do
|
||||
hl.clear()
|
||||
end
|
||||
HIGHLIGHTS = {}
|
||||
end
|
||||
|
||||
local function update()
|
||||
local mode = vim.fn.mode():sub(1, 1)
|
||||
if mode ~= 'n' then return end
|
||||
|
||||
local last_range = LAST_RANGE
|
||||
local bracket_range = Range.find_nearest_brackets()
|
||||
LAST_RANGE = bracket_range
|
||||
|
||||
if not bracket_range then return clear_highlights() end
|
||||
if bracket_range == last_range then return end
|
||||
|
||||
clear_highlights()
|
||||
|
||||
local open = Range.new(bracket_range.start, bracket_range.start, 'v')
|
||||
local close = Range.new(bracket_range.stop, bracket_range.stop, 'v')
|
||||
|
||||
HIGHLIGHTS = {
|
||||
open:highlight('MatchParen', { priority = 999 }),
|
||||
close:highlight('MatchParen', { priority = 999 }),
|
||||
}
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup('Matcher', { clear = true })
|
||||
vim.api.nvim_create_autocmd({ 'CursorMoved' }, {
|
||||
group = group,
|
||||
callback = update,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,15 +1,38 @@
|
||||
local vim_repeat = require 'u.repeat'
|
||||
local CodeWriter = require 'u.codewriter'
|
||||
local Range = require 'u.range'
|
||||
--
|
||||
-- Split/Join: toggles between single-line and multi-line forms for bracketed expressions.
|
||||
-- Example: { a, b, c } <-> {\n a,\n b,\n c\n}
|
||||
-- Maps to gS in normal mode.
|
||||
--
|
||||
local Range = require('u').Range
|
||||
local vim_repeat = require('u').repeat_
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param bracket_range Range
|
||||
---@param left string
|
||||
---@param right string
|
||||
--- @param bracket_range u.Range
|
||||
--- @param left string
|
||||
--- @param right string
|
||||
local function split(bracket_range, left, right)
|
||||
local code = CodeWriter.from_pos(bracket_range.start)
|
||||
code:write_raw(left)
|
||||
local bufnr = bracket_range.start.bufnr
|
||||
local first_line = Range.from_line(bufnr, bracket_range.start.lnum):text()
|
||||
local ws = first_line:match '^%s*'
|
||||
local expandtab = vim.bo[bufnr].expandtab
|
||||
local shiftwidth = vim.bo[bufnr].shiftwidth
|
||||
|
||||
local indent_str, base_indent
|
||||
if expandtab then
|
||||
indent_str = string.rep(' ', shiftwidth)
|
||||
base_indent = math.floor(#ws / shiftwidth)
|
||||
else
|
||||
indent_str = '\t'
|
||||
base_indent = #ws
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
local function write(line, indent_offset)
|
||||
table.insert(lines, indent_str:rep(base_indent + (indent_offset or 0)) .. line)
|
||||
end
|
||||
|
||||
table.insert(lines, left)
|
||||
|
||||
local curr = bracket_range.start:next()
|
||||
if curr == nil then return end
|
||||
@@ -27,7 +50,7 @@ local function split(bracket_range, left, right)
|
||||
if vim.tbl_contains({ ',', ';' }, curr:char()) then
|
||||
-- accumulate item:
|
||||
local item = vim.trim(Range.new(last_start, curr):text())
|
||||
if item ~= '' then code:indent():write(item) end
|
||||
if item ~= '' then write(item, 1) end
|
||||
|
||||
local next_last_start = curr:next()
|
||||
if next_last_start == nil then break end
|
||||
@@ -45,24 +68,28 @@ local function split(bracket_range, left, right)
|
||||
local pos_before_right = bracket_range.stop:must_next(-1)
|
||||
if last_start < pos_before_right then
|
||||
local item = vim.trim(Range.new(last_start, pos_before_right):text())
|
||||
if item ~= '' then code:indent():write(item) end
|
||||
if item ~= '' then write(item, 1) end
|
||||
end
|
||||
|
||||
code:write(right)
|
||||
bracket_range:replace(code.lines)
|
||||
write(right)
|
||||
bracket_range:replace(lines)
|
||||
end
|
||||
|
||||
---@param bracket_range Range
|
||||
---@param left string
|
||||
---@param right string
|
||||
--- @param bracket_range u.Range
|
||||
--- @param left string
|
||||
--- @param right string
|
||||
local function join(bracket_range, left, right)
|
||||
local inner_range = Range.new(bracket_range.start:must_next(), bracket_range.stop:must_next(-1), bracket_range.mode)
|
||||
local newline = vim
|
||||
.iter(inner_range:lines())
|
||||
:map(function(l) return vim.trim(l) end)
|
||||
:filter(function(l) return l ~= '' end)
|
||||
:join ' '
|
||||
bracket_range:replace { left .. newline .. right }
|
||||
local inner_range = bracket_range:shrink(1)
|
||||
if inner_range then
|
||||
local newline = vim
|
||||
.iter(inner_range:lines())
|
||||
:map(function(l) return vim.trim(l) end)
|
||||
:filter(function(l) return l ~= '' end)
|
||||
:join ' '
|
||||
bracket_range:replace { left .. newline .. right }
|
||||
else
|
||||
bracket_range:replace { left .. right }
|
||||
end
|
||||
end
|
||||
|
||||
local function splitjoin()
|
||||
@@ -80,7 +107,7 @@ local function splitjoin()
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
vim.keymap.set('n', 'gS', function() vim_repeat.run(splitjoin) end)
|
||||
vim.keymap.set('n', 'gS', function() vim_repeat.run_repeatable(splitjoin) end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
local vim_repeat = require 'u.repeat'
|
||||
local opkeymap = require 'u.opkeymap'
|
||||
local Pos = require 'u.pos'
|
||||
local Range = require 'u.range'
|
||||
local Buffer = require 'u.buffer'
|
||||
local CodeWriter = require 'u.codewriter'
|
||||
--
|
||||
-- Surround: add, change, and delete surrounding characters (quotes, brackets, HTML tags).
|
||||
--
|
||||
-- Mappings:
|
||||
-- ys{motion}{char} - add surround (e.g., ysiw" surrounds word with quotes)
|
||||
-- cs{from}{to} - change surround (e.g., cs"' changes " to ')
|
||||
-- ds{char} - delete surround (e.g., ds" removes surrounding quotes)
|
||||
-- S{char} - surround visual selection
|
||||
--
|
||||
local Range = require('u').Range
|
||||
local vim_repeat = require('u').repeat_
|
||||
|
||||
local M = {}
|
||||
|
||||
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
||||
|
||||
local surrounds = {
|
||||
[')'] = { left = '(', right = ')' },
|
||||
['('] = { left = '( ', right = ' )' },
|
||||
@@ -21,36 +28,37 @@ local surrounds = {
|
||||
['`'] = { left = '`', right = '`' },
|
||||
}
|
||||
|
||||
---@return { left: string; right: string }|nil
|
||||
--- @type { left: string, right: string } | nil
|
||||
local CACHED_BOUNDS = nil
|
||||
|
||||
--- @return { left: string, right: string }|nil
|
||||
local function prompt_for_bounds()
|
||||
if vim_repeat.is_repeating() then return CACHED_BOUNDS end
|
||||
|
||||
local cn = vim.fn.getchar()
|
||||
-- Check for non-printable characters:
|
||||
if type(cn) ~= 'number' or cn < 32 or cn > 126 then return end
|
||||
local c = vim.fn.nr2char(cn)
|
||||
|
||||
if c == '<' then
|
||||
-- Surround with a tag:
|
||||
vim.keymap.set('c', '>', '><CR>')
|
||||
local tag = '<' .. vim.fn.input '<'
|
||||
if tag == '<' then return end
|
||||
vim.keymap.del('c', '>')
|
||||
local endtag = '</' .. tag:sub(2):match '[^ >]*' .. '>'
|
||||
-- selene: allow(global_usage)
|
||||
return { left = tag, right = endtag }
|
||||
CACHED_BOUNDS = { left = tag, right = endtag }
|
||||
return CACHED_BOUNDS
|
||||
else
|
||||
-- Default surround:
|
||||
return (surrounds)[c] or { left = c, right = c }
|
||||
CACHED_BOUNDS = surrounds[c] or { left = c, right = c }
|
||||
return CACHED_BOUNDS
|
||||
end
|
||||
end
|
||||
|
||||
---@param range Range
|
||||
---@param bounds { left: string; right: string }
|
||||
--- @param range u.Range
|
||||
--- @param bounds { left: string, right: string }
|
||||
local function do_surround(range, bounds)
|
||||
local left = bounds.left
|
||||
local right = bounds.right
|
||||
if range.mode == 'V' then
|
||||
-- If we are surrounding multiple lines, we don't care about
|
||||
-- space-padding:
|
||||
left = vim.trim(left)
|
||||
right = vim.trim(right)
|
||||
end
|
||||
@@ -58,125 +66,141 @@ local function do_surround(range, bounds)
|
||||
if range.mode == 'v' then
|
||||
range:replace(left .. range:text() .. right)
|
||||
elseif range.mode == 'V' then
|
||||
local buf = Buffer.current()
|
||||
local cw = CodeWriter.from_line(buf:line0(range.start.lnum):text(), buf.buf)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local first_line = Range.from_line(bufnr, range.start.lnum):text()
|
||||
local ws = first_line:match '^%s*'
|
||||
local expandtab = vim.bo[bufnr].expandtab
|
||||
local shiftwidth = vim.bo[bufnr].shiftwidth
|
||||
|
||||
-- write the left bound at the current indent level:
|
||||
cw:write(left)
|
||||
local indent_str, base_indent
|
||||
if expandtab then
|
||||
indent_str = string.rep(' ', shiftwidth)
|
||||
base_indent = math.floor(#ws / shiftwidth)
|
||||
else
|
||||
indent_str = '\t'
|
||||
base_indent = #ws
|
||||
end
|
||||
|
||||
local curr_ident_prefix = cw.indent_str:rep(cw.indent_level)
|
||||
cw:indent(function(cw2)
|
||||
for _, line in ipairs(range:lines()) do
|
||||
-- trim the current indent prefix from the line:
|
||||
if line:sub(1, #curr_ident_prefix) == curr_ident_prefix then
|
||||
--
|
||||
line = line:sub(#curr_ident_prefix + 1)
|
||||
end
|
||||
local lines = {}
|
||||
local function write(line, indent_offset)
|
||||
table.insert(lines, indent_str:rep(base_indent + (indent_offset or 0)) .. line)
|
||||
end
|
||||
|
||||
cw2:write(line)
|
||||
end
|
||||
end)
|
||||
write(left)
|
||||
local indent_prefix = indent_str:rep(base_indent)
|
||||
for _, line in ipairs(range:lines()) do
|
||||
if line:sub(1, #indent_prefix) == indent_prefix then line = line:sub(#indent_prefix + 1) end
|
||||
write(line, 1)
|
||||
end
|
||||
write(right)
|
||||
|
||||
-- write the right bound at the current indent level:
|
||||
cw:write(right)
|
||||
|
||||
range:replace(cw.lines)
|
||||
range:replace(lines)
|
||||
end
|
||||
|
||||
range.start:save_to_pos '.'
|
||||
end
|
||||
|
||||
-- Add surround:
|
||||
--- @param ty 'line' | 'char' | 'block'
|
||||
function _G.MySurroundOpFunc(ty)
|
||||
if ty == 'block' then
|
||||
-- We won't handle block-selection:
|
||||
return
|
||||
end
|
||||
|
||||
local range = Range.from_op_func(ty)
|
||||
local hl
|
||||
if not vim_repeat.is_repeating() then hl = range:highlight('IncSearch', { priority = 999 }) end
|
||||
|
||||
local bounds = prompt_for_bounds()
|
||||
if hl then hl.clear() end
|
||||
if bounds == nil then return end
|
||||
|
||||
do_surround(range, bounds)
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
require('u.repeat').setup()
|
||||
|
||||
-- Visual
|
||||
vim.keymap.set('v', 'S', function()
|
||||
local c = vim.fn.getcharstr()
|
||||
vim.keymap.set('x', 'S', function()
|
||||
local range = Range.from_vtext()
|
||||
local bounds = surrounds[c] or { left = c, right = c }
|
||||
vim_repeat.run(function()
|
||||
do_surround(range, bounds)
|
||||
-- this is a visual mapping: end in normal mode:
|
||||
vim.cmd { cmd = 'normal', args = { '' }, bang = true }
|
||||
end)
|
||||
local bounds = prompt_for_bounds()
|
||||
if bounds == nil then return end
|
||||
|
||||
do_surround(range, bounds)
|
||||
-- this is a visual mapping: end in normal mode:
|
||||
vim.cmd.normal(ESC)
|
||||
end, { noremap = true, silent = true })
|
||||
|
||||
-- Change
|
||||
vim.keymap.set('n', 'cs', function()
|
||||
local from_cn = vim.fn.getchar()
|
||||
local from_cn = vim.fn.getchar() --[[@as number]]
|
||||
-- Check for non-printable characters:
|
||||
if from_cn < 32 or from_cn > 126 then return end
|
||||
local from_c = vim.fn.nr2char(from_cn)
|
||||
local from = surrounds[from_c] or { left = from_c, right = from_c }
|
||||
local function get_fresh_arange()
|
||||
local arange = Range.from_text_object('a' .. from_c, { user_defined = true })
|
||||
if arange == nil then return nil end
|
||||
if from_c == 'q' then
|
||||
from.left = arange.start:char()
|
||||
from.right = arange.stop:char()
|
||||
|
||||
vim_repeat.run_repeatable(function()
|
||||
local from_c = vim.fn.nr2char(from_cn)
|
||||
local from = surrounds[from_c] or { left = from_c, right = from_c }
|
||||
local function get_fresh_arange()
|
||||
local arange = Range.from_motion('a' .. from_c, { user_defined = true })
|
||||
if arange == nil then return end
|
||||
if from_c == 'q' then
|
||||
from.left = arange.start:char()
|
||||
from.right = arange.stop:char()
|
||||
end
|
||||
return arange
|
||||
end
|
||||
return arange
|
||||
end
|
||||
|
||||
local arange = get_fresh_arange()
|
||||
if arange == nil then return nil end
|
||||
local arange = get_fresh_arange()
|
||||
if arange == nil then return end
|
||||
|
||||
local hl_info1 = Range.new(arange.start, arange.start, 'v'):highlight('IncSearch', { priority = 999 })
|
||||
local hl_info2 = Range.new(arange.stop, arange.stop, 'v'):highlight('IncSearch', { priority = 999 })
|
||||
local hl_clear = function()
|
||||
if hl_info1 then hl_info1.clear() end
|
||||
if hl_info2 then hl_info2.clear() end
|
||||
end
|
||||
local hl_info1 = vim_repeat.is_repeating() and nil
|
||||
or Range.new(arange.start, arange.start, 'v'):highlight('IncSearch', { priority = 999 })
|
||||
local hl_info2 = vim_repeat.is_repeating() and nil
|
||||
or Range.new(arange.stop, arange.stop, 'v'):highlight('IncSearch', { priority = 999 })
|
||||
local hl_clear = function()
|
||||
if hl_info1 then hl_info1.clear() end
|
||||
if hl_info2 then hl_info2.clear() end
|
||||
end
|
||||
|
||||
local to = prompt_for_bounds()
|
||||
hl_clear()
|
||||
if to == nil then return end
|
||||
|
||||
vim_repeat.run(function()
|
||||
-- Re-fetch the arange, just in case this action is being repeated:
|
||||
arange = get_fresh_arange()
|
||||
if arange == nil then return nil end
|
||||
local to = prompt_for_bounds()
|
||||
hl_clear()
|
||||
if to == nil then return end
|
||||
|
||||
if from_c == 't' then
|
||||
-- For tags, we want to replace the inner text, not the tag:
|
||||
local irange = Range.from_text_object('i' .. from_c, { user_defined = true })
|
||||
if arange == nil or irange == nil then return nil end
|
||||
local irange = Range.from_motion('i' .. from_c, { user_defined = true })
|
||||
if arange == nil or irange == nil then return end
|
||||
|
||||
local lrange = Range.new(arange.start, irange.start:must_next(-1))
|
||||
local rrange = Range.new(irange.stop:must_next(1), arange.stop)
|
||||
local lrange, rrange = arange:difference(irange)
|
||||
if not lrange or not rrange then return end
|
||||
|
||||
rrange:replace(to.right)
|
||||
lrange:replace(to.left)
|
||||
else
|
||||
-- replace `from.right` with `to.right`:
|
||||
local last_line = arange:line0(-1).text() --[[@as string]]
|
||||
local from_right_match = last_line:match(vim.pesc(from.right) .. '$')
|
||||
if from_right_match then
|
||||
local match_start = arange.stop:clone()
|
||||
match_start.col = match_start.col - #from_right_match + 1
|
||||
Range.new(match_start, arange.stop):replace(to.right)
|
||||
end
|
||||
local right_text = arange:sub(-1, -#from.right)
|
||||
right_text:replace(to.right)
|
||||
|
||||
-- replace `from.left` with `to.left`:
|
||||
local first_line = arange:line0(0).text() --[[@as string]]
|
||||
local from_left_match = first_line:match('^' .. vim.pesc(from.left))
|
||||
if from_left_match then
|
||||
local match_end = arange.start:clone()
|
||||
match_end.col = match_end.col + #from_left_match - 1
|
||||
Range.new(arange.start, match_end):replace(to.left)
|
||||
end
|
||||
local left_text = arange:sub(1, #from.left)
|
||||
left_text:replace(to.left)
|
||||
end
|
||||
end)
|
||||
end, { noremap = true, silent = true })
|
||||
|
||||
-- Delete
|
||||
local CACHED_DELETE_FROM = nil
|
||||
vim.keymap.set('n', 'ds', function()
|
||||
local txt_obj = vim.fn.getcharstr()
|
||||
vim_repeat.run(function()
|
||||
local buf = Buffer.current()
|
||||
local irange = Range.from_text_object('i' .. txt_obj)
|
||||
local arange = Range.from_text_object('a' .. txt_obj)
|
||||
if arange == nil or irange == nil then return nil end
|
||||
vim_repeat.run_repeatable(function()
|
||||
local txt_obj = vim_repeat.is_repeating() and CACHED_DELETE_FROM or vim.fn.getcharstr()
|
||||
CACHED_DELETE_FROM = txt_obj
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local irange = Range.from_motion('i' .. txt_obj)
|
||||
local arange = Range.from_motion('a' .. txt_obj)
|
||||
if arange == nil or irange == nil then return end
|
||||
local starting_cursor_pos = arange.start:clone()
|
||||
|
||||
-- Now, replace `arange` with the content of `irange`. If `arange` was multiple lines,
|
||||
@@ -187,28 +211,19 @@ function M.setup()
|
||||
-- Dedenting moves the cursor, so we need to set the cursor to a consistent starting spot:
|
||||
arange.start:save_to_pos '.'
|
||||
-- Dedenting also changed the inner text, so re-acquire it:
|
||||
arange = Range.from_text_object('a' .. txt_obj)
|
||||
irange = Range.from_text_object('i' .. txt_obj)
|
||||
arange = Range.from_motion('a' .. txt_obj)
|
||||
irange = Range.from_motion('i' .. txt_obj)
|
||||
if arange == nil or irange == nil then return end -- should never be true
|
||||
arange:replace(irange:lines())
|
||||
|
||||
local final_range = Range.new(
|
||||
arange.start,
|
||||
Pos.new(
|
||||
arange.stop.buf,
|
||||
irange.start.lnum + (arange.stop.lnum + arange.start.lnum),
|
||||
arange.stop.col,
|
||||
arange.stop.off
|
||||
),
|
||||
irange.mode
|
||||
)
|
||||
-- `arange:replace(..)` updates its own `stop` position, so we will use
|
||||
-- `arange` as the final resulting range that holds the modified text
|
||||
|
||||
-- delete last line, if it is empty:
|
||||
local last = buf:line0(final_range.stop.lnum)
|
||||
local last = Range.from_line(bufnr, arange.stop.lnum)
|
||||
if last:text():match '^%s*$' then last:replace(nil) end
|
||||
|
||||
-- delete first line, if it is empty:
|
||||
local first = buf:line0(final_range.start.lnum)
|
||||
local first = Range.from_line(bufnr, arange.start.lnum)
|
||||
if first:text():match '^%s*$' then first:replace(nil) end
|
||||
else
|
||||
-- trim start:
|
||||
@@ -220,35 +235,10 @@ function M.setup()
|
||||
end)
|
||||
end, { noremap = true, silent = true })
|
||||
|
||||
opkeymap('n', 'ys', function(range)
|
||||
local hl_info = range:highlight('IncSearch', { priority = 999 })
|
||||
|
||||
---@type { left: string; right: string }
|
||||
local bounds
|
||||
-- selene: allow(global_usage)
|
||||
if _G.my_surround_bounds ~= nil then
|
||||
-- This command was repeated with `.`, we don't need
|
||||
-- to prompt for the bounds:
|
||||
-- selene: allow(global_usage)
|
||||
bounds = _G.my_surround_bounds
|
||||
else
|
||||
local prompted_bounds = prompt_for_bounds()
|
||||
if prompted_bounds == nil and hl_info then return hl_info.clear() end
|
||||
if prompted_bounds then bounds = prompted_bounds end
|
||||
end
|
||||
|
||||
if hl_info then hl_info.clear() end
|
||||
do_surround(range, bounds)
|
||||
-- selene: allow(global_usage)
|
||||
_G.my_surround_bounds = nil
|
||||
|
||||
-- return repeatable injection
|
||||
return function()
|
||||
-- on_repeat, we "stage" the bounds that we were originally called with:
|
||||
-- selene: allow(global_usage)
|
||||
_G.my_surround_bounds = bounds
|
||||
end
|
||||
end)
|
||||
vim.keymap.set('n', 'ys', function()
|
||||
vim.o.operatorfunc = 'v:lua.MySurroundOpFunc'
|
||||
return 'g@'
|
||||
end, { expr = true })
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,58 +1,47 @@
|
||||
local utils = require 'u.utils'
|
||||
local Pos = require 'u.pos'
|
||||
local Range = require 'u.range'
|
||||
local Buffer = require 'u.buffer'
|
||||
--
|
||||
-- Custom text objects:
|
||||
-- ag - whole file
|
||||
-- a. - current line
|
||||
-- aq/iq - nearest quote (any type)
|
||||
-- a"/i", a'/i', a`/i` - specific quotes
|
||||
-- al"/il", al'/il', al`/il` - "last" quote (searches backward)
|
||||
-- alB/ilB, al]/il], alb/ilb, al>/il> - "last" bracket objects
|
||||
--
|
||||
local u = require 'u'
|
||||
local Pos = u.Pos
|
||||
local Range = u.Range
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
-- Select whole file:
|
||||
utils.define_text_object('ag', function() return Buffer.current():all() end)
|
||||
u.define_txtobj('ag', function() return Range.from_buf_text(0) end)
|
||||
|
||||
-- Select current line:
|
||||
utils.define_text_object('a.', function()
|
||||
local lnum = Pos.from_pos('.').lnum
|
||||
return Buffer.current():line0(lnum)
|
||||
end)
|
||||
u.define_txtobj('a.', function() return Range.from_line(0, Pos.from_pos('.').lnum) end)
|
||||
|
||||
-- Select the nearest quote:
|
||||
utils.define_text_object('aq', function() return Range.find_nearest_quotes() end)
|
||||
utils.define_text_object('iq', function()
|
||||
u.define_txtobj('aq', function() return Range.find_nearest_quotes() end)
|
||||
u.define_txtobj('iq', function()
|
||||
local range = Range.find_nearest_quotes()
|
||||
if range == nil then return end
|
||||
return range:shrink(1)
|
||||
end)
|
||||
|
||||
---Selects the next quote object (searches forward)
|
||||
---@param q string
|
||||
--- @param q string
|
||||
local function define_quote_obj(q)
|
||||
local function select_around()
|
||||
-- Operator mappings are effectively running in visual mode, the way
|
||||
-- `define_text_object` is implemented, so feed the keys `a${q}` to vim
|
||||
-- to select the appropriate text-object
|
||||
vim.cmd { cmd = 'normal', args = { 'a' .. q }, bang = true }
|
||||
local function select_around() return Range.from_motion('a' .. q) end
|
||||
|
||||
-- Now check on the visually selected text:
|
||||
local range = Range.from_vtext()
|
||||
if range:is_empty() then return range.start end
|
||||
range.start = range.start:find_next(1, q) or range.start
|
||||
range.stop = range.stop:find_next(-1, q) or range.stop
|
||||
return range
|
||||
end
|
||||
u.define_txtobj('a' .. q, function() return select_around() end)
|
||||
u.define_txtobj('i' .. q, function()
|
||||
local range = select_around()
|
||||
if range == nil or range:is_empty() then return range end
|
||||
|
||||
utils.define_text_object('a' .. q, function() return select_around() end)
|
||||
utils.define_text_object('i' .. q, function()
|
||||
local range_or_pos = select_around()
|
||||
if Range.is(range_or_pos) then
|
||||
local start_next = range_or_pos.start:next(1)
|
||||
local stop_prev = range_or_pos.stop:next(-1)
|
||||
if start_next > stop_prev then return start_next end
|
||||
|
||||
local range = range_or_pos:shrink(1)
|
||||
return range
|
||||
else
|
||||
return range_or_pos
|
||||
end
|
||||
local start_next = range.start:next(1) or range.start
|
||||
local stop_prev = range.stop:next(-1)
|
||||
if start_next > stop_prev then return Range.new(start_next) end
|
||||
return range:shrink(1) or range
|
||||
end)
|
||||
end
|
||||
define_quote_obj [["]]
|
||||
@@ -60,36 +49,26 @@ function M.setup()
|
||||
define_quote_obj [[`]]
|
||||
|
||||
---Selects the "last" quote object (searches backward)
|
||||
---@param q string
|
||||
--- @param q string
|
||||
local function define_last_quote_obj(q)
|
||||
local function select_around()
|
||||
local curr = Pos.from_pos('.'):find_next(-1, q)
|
||||
if not curr then return end
|
||||
-- Reset visual selection to current context:
|
||||
Range.new(curr, curr):set_visual_selection()
|
||||
vim.cmd.normal('a' .. q)
|
||||
local range = Range.from_vtext()
|
||||
if range:is_empty() then return range.start end
|
||||
range.start = range.start:find_next(1, q) or range.start
|
||||
range.stop = range.stop:find_next(-1, q) or range.stop
|
||||
return range
|
||||
curr:save_to_pos '.'
|
||||
return Range.from_motion('a' .. q)
|
||||
end
|
||||
|
||||
utils.define_text_object('al' .. q, function() return select_around() end)
|
||||
utils.define_text_object('il' .. q, function()
|
||||
local range_or_pos = select_around()
|
||||
if range_or_pos == nil then return end
|
||||
u.define_txtobj('al' .. q, function() return select_around() end)
|
||||
u.define_txtobj('il' .. q, function()
|
||||
local range = select_around()
|
||||
if range == nil or range:is_empty() then return range end
|
||||
|
||||
if Range.is(range_or_pos) then
|
||||
local start_next = range_or_pos.start:next(1)
|
||||
local stop_prev = range_or_pos.stop:next(-1)
|
||||
if start_next > stop_prev then return start_next end
|
||||
local start_next = range.start:next(1) or range.start
|
||||
local stop_prev = range.stop:next(-1)
|
||||
if start_next > stop_prev then return Range.new(start_next) end
|
||||
|
||||
local range = range_or_pos:shrink(1)
|
||||
return range
|
||||
else
|
||||
return range_or_pos
|
||||
end
|
||||
return range:shrink(1) or range
|
||||
end)
|
||||
end
|
||||
define_last_quote_obj [["]]
|
||||
@@ -111,8 +90,8 @@ function M.setup()
|
||||
local keybinds = { ... }
|
||||
table.insert(keybinds, b)
|
||||
for _, k in ipairs(keybinds) do
|
||||
utils.define_text_object('al' .. k, function() return select_around() end)
|
||||
utils.define_text_object('il' .. k, function()
|
||||
u.define_txtobj('al' .. k, function() return select_around() end)
|
||||
u.define_txtobj('il' .. k, function()
|
||||
local range = select_around()
|
||||
return range and range:shrink(1)
|
||||
end)
|
||||
|
||||
1
library/busted
Submodule
1
library/busted
Submodule
Submodule library/busted added at 5ed85d0e01
1
library/luv
Submodule
1
library/luv
Submodule
Submodule library/luv added at 3615eb12c9
@@ -1,72 +0,0 @@
|
||||
local Range = require 'u.range'
|
||||
|
||||
---@class Buffer
|
||||
---@field buf number
|
||||
local Buffer = {}
|
||||
|
||||
---@param buf? number
|
||||
---@return Buffer
|
||||
function Buffer.from_nr(buf)
|
||||
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||
local b = { buf = buf }
|
||||
setmetatable(b, { __index = Buffer })
|
||||
return b
|
||||
end
|
||||
|
||||
---@return Buffer
|
||||
function Buffer.current() return Buffer.from_nr(0) end
|
||||
|
||||
---@param listed boolean
|
||||
---@param scratch boolean
|
||||
---@return Buffer
|
||||
function Buffer.create(listed, scratch) return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch)) end
|
||||
|
||||
function Buffer:set_tmp_options()
|
||||
self:set_option('bufhidden', 'delete')
|
||||
self:set_option('buflisted', false)
|
||||
self:set_option('buftype', 'nowrite')
|
||||
end
|
||||
|
||||
---@param nm string
|
||||
function Buffer:get_option(nm) return vim.api.nvim_get_option_value(nm, { buf = self.buf }) end
|
||||
|
||||
---@param nm string
|
||||
function Buffer:set_option(nm, val) return vim.api.nvim_set_option_value(nm, val, { buf = self.buf }) end
|
||||
|
||||
---@param nm string
|
||||
function Buffer:get_var(nm) return vim.api.nvim_buf_get_var(self.buf, nm) end
|
||||
|
||||
---@param nm string
|
||||
function Buffer:set_var(nm, val) return vim.api.nvim_buf_set_var(self.buf, nm, val) end
|
||||
|
||||
function Buffer:line_count() return vim.api.nvim_buf_line_count(self.buf) end
|
||||
|
||||
function Buffer:all() return Range.from_buf_text(self.buf) end
|
||||
|
||||
function Buffer:is_empty() return self:line_count() == 1 and self:line0(0):text() == '' end
|
||||
|
||||
---@param line string
|
||||
function Buffer:append_line(line)
|
||||
local start = -1
|
||||
if self:is_empty() then start = -2 end
|
||||
vim.api.nvim_buf_set_lines(self.buf, start, -1, false, { line })
|
||||
end
|
||||
|
||||
---@param num number 0-based line index
|
||||
function Buffer:line0(num)
|
||||
if num < 0 then return self:line0(self:line_count() + num) end
|
||||
return Range.from_line(self.buf, num)
|
||||
end
|
||||
|
||||
---@param start number 0-based line index
|
||||
---@param stop number 0-based line index
|
||||
function Buffer:lines(start, stop) return Range.from_lines(self.buf, start, stop) end
|
||||
|
||||
---@param txt_obj string
|
||||
---@param opts? { contains_cursor?: boolean; pos?: Pos }
|
||||
function Buffer:text_object(txt_obj, opts)
|
||||
opts = vim.tbl_extend('force', opts or {}, { buf = self.buf })
|
||||
return Range.from_text_object(txt_obj, opts)
|
||||
end
|
||||
|
||||
return Buffer
|
||||
@@ -1,76 +0,0 @@
|
||||
local Buffer = require 'u.buffer'
|
||||
|
||||
---@class CodeWriter
|
||||
---@field lines string[]
|
||||
---@field indent_level number
|
||||
---@field indent_str string
|
||||
local CodeWriter = {}
|
||||
|
||||
---@param indent_level? number
|
||||
---@param indent_str? string
|
||||
---@return CodeWriter
|
||||
function CodeWriter.new(indent_level, indent_str)
|
||||
if indent_level == nil then indent_level = 0 end
|
||||
if indent_str == nil then indent_str = ' ' end
|
||||
|
||||
local cw = {
|
||||
lines = {},
|
||||
indent_level = indent_level,
|
||||
indent_str = indent_str,
|
||||
}
|
||||
setmetatable(cw, { __index = CodeWriter })
|
||||
return cw
|
||||
end
|
||||
|
||||
---@param p Pos
|
||||
function CodeWriter.from_pos(p)
|
||||
local line = Buffer.from_nr(p.buf):line0(p.lnum):text()
|
||||
return CodeWriter.from_line(line, p.buf)
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@param buf? number
|
||||
function CodeWriter.from_line(line, buf)
|
||||
if buf == nil then buf = vim.api.nvim_get_current_buf() end
|
||||
|
||||
local ws = line:match '^%s*'
|
||||
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = buf })
|
||||
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = buf })
|
||||
|
||||
local indent_level = 0
|
||||
local indent_str = ''
|
||||
if expandtab then
|
||||
while #indent_str < shiftwidth do
|
||||
indent_str = indent_str .. ' '
|
||||
end
|
||||
indent_level = #ws / shiftwidth
|
||||
else
|
||||
indent_str = '\t'
|
||||
indent_level = #ws
|
||||
end
|
||||
|
||||
return CodeWriter.new(indent_level, indent_str)
|
||||
end
|
||||
|
||||
---@param line string
|
||||
function CodeWriter:write_raw(line)
|
||||
if line:find '\n' then error 'line contains newline character' end
|
||||
table.insert(self.lines, line)
|
||||
end
|
||||
|
||||
---@param line string
|
||||
function CodeWriter:write(line) self:write_raw(self.indent_str:rep(self.indent_level) .. line) end
|
||||
|
||||
---@param f? fun(cw: CodeWriter):any
|
||||
function CodeWriter:indent(f)
|
||||
local cw = {
|
||||
lines = self.lines,
|
||||
indent_level = self.indent_level + 1,
|
||||
indent_str = self.indent_str,
|
||||
}
|
||||
setmetatable(cw, { __index = CodeWriter })
|
||||
if f ~= nil then f(cw) end
|
||||
return cw
|
||||
end
|
||||
|
||||
return CodeWriter
|
||||
@@ -1,43 +0,0 @@
|
||||
local Range = require 'u.range'
|
||||
local vim_repeat = require 'u.repeat'
|
||||
|
||||
---@type fun(range: Range): nil|(fun():any)
|
||||
local __U__OpKeymapOpFunc_rhs = nil
|
||||
|
||||
--- This is the global utility function used for operatorfunc
|
||||
--- in opkeymap
|
||||
---@type nil|fun(range: Range): fun():any|nil
|
||||
---@param ty 'line'|'char'|'block'
|
||||
-- selene: allow(unused_variable)
|
||||
function __U__OpKeymapOpFunc(ty)
|
||||
if __U__OpKeymapOpFunc_rhs ~= nil then
|
||||
local range = Range.from_op_func(ty)
|
||||
local repeat_inject = __U__OpKeymapOpFunc_rhs(range)
|
||||
|
||||
vim_repeat.set(function()
|
||||
vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc'
|
||||
if repeat_inject ~= nil and type(repeat_inject) == 'function' then repeat_inject() end
|
||||
vim_repeat.native_repeat()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- Registers a function that operates on a text-object, triggered by the given prefix (lhs).
|
||||
--- It works in the following way:
|
||||
--- 1. An expression-map is set, so that whatever the callback returns is executed by Vim (in this case `g@`)
|
||||
--- g@: tells vim to way for a motion, and then call operatorfunc.
|
||||
--- 2. The operatorfunc is set to a lua function that computes the range being operated over, that
|
||||
--- then calls the original passed callback with said range.
|
||||
---@param mode string|string[]
|
||||
---@param lhs string
|
||||
---@param rhs fun(range: Range): nil|(fun():any) This function may return another function, which is called whenever the operator is repeated
|
||||
---@param opts? vim.keymap.set.Opts
|
||||
local function opkeymap(mode, lhs, rhs, opts)
|
||||
vim.keymap.set(mode, lhs, function()
|
||||
__U__OpKeymapOpFunc_rhs = rhs
|
||||
vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc'
|
||||
return 'g@'
|
||||
end, vim.tbl_extend('force', opts or {}, { expr = true }))
|
||||
end
|
||||
|
||||
return opkeymap
|
||||
239
lua/u/pos.lua
239
lua/u/pos.lua
@@ -1,239 +0,0 @@
|
||||
local MAX_COL = vim.v.maxcol
|
||||
|
||||
---@param buf number
|
||||
---@param lnum number
|
||||
local function line_text(buf, lnum) return vim.api.nvim_buf_get_lines(buf, lnum, lnum + 1, false)[1] end
|
||||
|
||||
---@class Pos
|
||||
---@field buf number buffer number
|
||||
---@field lnum number 1-based line index
|
||||
---@field col number 1-based column index
|
||||
---@field off number
|
||||
local Pos = {}
|
||||
Pos.MAX_COL = MAX_COL
|
||||
|
||||
---@param buf? number
|
||||
---@param lnum number
|
||||
---@param col number
|
||||
---@param off? number
|
||||
---@return Pos
|
||||
function Pos.new(buf, lnum, col, off)
|
||||
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||
if off == nil then off = 0 end
|
||||
local pos = {
|
||||
buf = buf,
|
||||
lnum = lnum,
|
||||
col = col,
|
||||
off = off,
|
||||
}
|
||||
|
||||
local function str()
|
||||
if pos.off ~= 0 then
|
||||
return string.format('Pos(%d:%d){buf=%d, off=%d}', pos.lnum, pos.col, pos.buf, pos.off)
|
||||
else
|
||||
return string.format('Pos(%d:%d){buf=%d}', pos.lnum, pos.col, pos.buf)
|
||||
end
|
||||
end
|
||||
setmetatable(pos, {
|
||||
__index = Pos,
|
||||
__tostring = str,
|
||||
__lt = Pos.__lt,
|
||||
__le = Pos.__le,
|
||||
__eq = Pos.__eq,
|
||||
})
|
||||
return pos
|
||||
end
|
||||
|
||||
function Pos.is(x)
|
||||
local mt = getmetatable(x)
|
||||
return mt and mt.__index == Pos
|
||||
end
|
||||
|
||||
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
||||
function Pos.__le(a, b) return a < b or a == b end
|
||||
function Pos.__eq(a, b) return a.lnum == b.lnum and a.col == b.col end
|
||||
|
||||
---@param name string
|
||||
---@return Pos
|
||||
function Pos.from_pos(name)
|
||||
local p = vim.fn.getpos(name)
|
||||
local col = p[3]
|
||||
if col ~= MAX_COL then col = col - 1 end
|
||||
return Pos.new(p[1], p[2] - 1, col, p[4])
|
||||
end
|
||||
|
||||
function Pos:clone() return Pos.new(self.buf, self.lnum, self.col, self.off) end
|
||||
|
||||
---@return boolean
|
||||
function Pos:is_col_max() return self.col == MAX_COL end
|
||||
|
||||
---@return number[]
|
||||
function Pos:as_vim() return { self.buf, self.lnum, self.col, self.off } end
|
||||
|
||||
--- Normalize the position to a real position (take into account vim.v.maxcol).
|
||||
function Pos:as_real()
|
||||
local col = self.col
|
||||
if self:is_col_max() then
|
||||
-- We could use utilities in this file to get the given line, but
|
||||
-- since this is a low-level function, we are going to optimize and
|
||||
-- use the API directly:
|
||||
col = #line_text(self.buf, self.lnum) - 1
|
||||
end
|
||||
return Pos.new(self.buf, self.lnum, col, self.off)
|
||||
end
|
||||
|
||||
---@param pos string
|
||||
function Pos:save_to_pos(pos)
|
||||
if pos == '.' then
|
||||
vim.api.nvim_win_set_cursor(0, { self.lnum + 1, self.col })
|
||||
return
|
||||
end
|
||||
|
||||
local p = self:as_real()
|
||||
vim.fn.setpos(pos, { p.buf, p.lnum + 1, p.col + 1, p.off })
|
||||
end
|
||||
|
||||
---@param mark string
|
||||
function Pos:save_to_mark(mark)
|
||||
local p = self:as_real()
|
||||
vim.api.nvim_buf_set_mark(p.buf, mark, p.lnum + 1, p.col, {})
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Pos:char()
|
||||
local line = line_text(self.buf, self.lnum)
|
||||
if line == nil then return '' end
|
||||
return line:sub(self.col + 1, self.col + 1)
|
||||
end
|
||||
|
||||
---@param dir? -1|1
|
||||
---@param must? boolean
|
||||
---@return Pos|nil
|
||||
function Pos:next(dir, must)
|
||||
if must == nil then must = false end
|
||||
|
||||
if dir == nil or dir == 1 then
|
||||
-- Next:
|
||||
local num_lines = vim.api.nvim_buf_line_count(self.buf)
|
||||
local last_line = line_text(self.buf, num_lines - 1) -- buf:line0(-1)
|
||||
if self.lnum == num_lines - 1 and self.col == (#last_line - 1) then
|
||||
if must then error 'error in Pos:next(): Pos:next() returned nil' end
|
||||
return nil
|
||||
end
|
||||
|
||||
local col = self.col + 1
|
||||
local line = self.lnum
|
||||
local line_max_col = #line_text(self.buf, self.lnum) - 1
|
||||
if col > line_max_col then
|
||||
col = 0
|
||||
line = line + 1
|
||||
end
|
||||
return Pos.new(self.buf, line, col, self.off)
|
||||
else
|
||||
-- Previous:
|
||||
if self.col == 0 and self.lnum == 0 then
|
||||
if must then error 'error in Pos:next(): Pos:next() returned nil' end
|
||||
return nil
|
||||
end
|
||||
|
||||
local col = self.col - 1
|
||||
local line = self.lnum
|
||||
local prev_line_max_col = #(line_text(self.buf, self.lnum - 1) or '') - 1
|
||||
if col < 0 then
|
||||
col = math.max(prev_line_max_col, 0)
|
||||
line = line - 1
|
||||
end
|
||||
return Pos.new(self.buf, line, col, self.off)
|
||||
end
|
||||
end
|
||||
|
||||
---@param dir? -1|1
|
||||
function Pos:must_next(dir)
|
||||
local next = self:next(dir, true)
|
||||
if next == nil then error 'unreachable' end
|
||||
return next
|
||||
end
|
||||
|
||||
---@param dir -1|1
|
||||
---@param predicate fun(p: Pos): boolean
|
||||
---@param test_current? boolean
|
||||
function Pos:next_while(dir, predicate, test_current)
|
||||
if test_current and not predicate(self) then return end
|
||||
local curr = self
|
||||
while true do
|
||||
local next = curr:next(dir)
|
||||
if next == nil or not predicate(next) then break end
|
||||
curr = next
|
||||
end
|
||||
return curr
|
||||
end
|
||||
|
||||
---@param dir -1|1
|
||||
---@param predicate string|fun(p: Pos): boolean
|
||||
function Pos:find_next(dir, predicate)
|
||||
if type(predicate) == 'string' then
|
||||
local s = predicate
|
||||
predicate = function(p) return s == p:char() end
|
||||
end
|
||||
|
||||
---@type Pos|nil
|
||||
local curr = self
|
||||
while curr ~= nil do
|
||||
if predicate(curr) then return curr end
|
||||
curr = curr:next(dir)
|
||||
end
|
||||
return curr
|
||||
end
|
||||
|
||||
--- Finds the matching bracket/paren for the current position.
|
||||
---@param max_chars? number|nil
|
||||
---@param invocations? Pos[]
|
||||
---@return Pos|nil
|
||||
function Pos:find_match(max_chars, invocations)
|
||||
if invocations == nil then invocations = {} end
|
||||
if vim.tbl_contains(invocations, function(p) return self == p end, { predicate = true }) then return nil end
|
||||
table.insert(invocations, self)
|
||||
|
||||
local openers = { '{', '[', '(', '<' }
|
||||
local closers = { '}', ']', ')', '>' }
|
||||
local c = self:char()
|
||||
local is_opener = vim.tbl_contains(openers, c)
|
||||
local is_closer = vim.tbl_contains(closers, c)
|
||||
if not is_opener and not is_closer then return nil end
|
||||
|
||||
local i, _ = vim.iter(is_opener and openers or closers):enumerate():find(function(_, c2) return c == c2 end)
|
||||
local c_match = (is_opener and closers or openers)[i]
|
||||
|
||||
---@type Pos|nil
|
||||
local cur = self
|
||||
---@return Pos|nil
|
||||
local function adv()
|
||||
if cur == nil then return nil end
|
||||
|
||||
if max_chars ~= nil then
|
||||
max_chars = max_chars - 1
|
||||
if max_chars < 0 then return nil end
|
||||
end
|
||||
|
||||
return cur:next(is_opener and 1 or -1)
|
||||
end
|
||||
|
||||
-- scan until we find a match:
|
||||
cur = adv()
|
||||
while cur ~= nil and cur:char() ~= c_match do
|
||||
cur = adv()
|
||||
if cur == nil then break end
|
||||
|
||||
local c2 = cur:char()
|
||||
if c2 == c_match then break end
|
||||
|
||||
if vim.tbl_contains(openers, c2) or vim.tbl_contains(closers, c2) then
|
||||
cur = cur:find_match(max_chars, invocations)
|
||||
cur = adv() -- move past the match
|
||||
end
|
||||
end
|
||||
|
||||
return cur
|
||||
end
|
||||
|
||||
return Pos
|
||||
522
lua/u/range.lua
522
lua/u/range.lua
@@ -1,522 +0,0 @@
|
||||
local Pos = require 'u.pos'
|
||||
local State = require 'u.state'
|
||||
|
||||
local orig_on_yank = vim.highlight.on_yank
|
||||
local on_yank_enabled = true;
|
||||
(vim.highlight --[[@as any]]).on_yank = function(opts)
|
||||
if not on_yank_enabled then return end
|
||||
return orig_on_yank(opts)
|
||||
end
|
||||
|
||||
---@class Range
|
||||
---@field start Pos
|
||||
---@field stop Pos|nil
|
||||
---@field mode 'v'|'V'
|
||||
local Range = {}
|
||||
|
||||
---@param start Pos
|
||||
---@param stop Pos|nil
|
||||
---@param mode? 'v'|'V'
|
||||
---@return Range
|
||||
function Range.new(start, stop, mode)
|
||||
if stop ~= nil and stop < start then
|
||||
start, stop = stop, start
|
||||
end
|
||||
|
||||
local r = { start = start, stop = stop, mode = mode or 'v' }
|
||||
local function str()
|
||||
---@param p Pos
|
||||
local function posstr(p)
|
||||
if p == nil then
|
||||
return 'nil'
|
||||
elseif p.off ~= 0 then
|
||||
return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off)
|
||||
else
|
||||
return string.format('Pos(%d:%d)', p.lnum, p.col)
|
||||
end
|
||||
end
|
||||
|
||||
local _1 = posstr(r.start)
|
||||
local _2 = posstr(r.stop)
|
||||
return string.format('Range{buf=%d, mode=%s, start=%s, stop=%s}', r.start.buf, r.mode, _1, _2)
|
||||
end
|
||||
setmetatable(r, { __index = Range, __tostring = str })
|
||||
return r
|
||||
end
|
||||
|
||||
function Range.is(x)
|
||||
local mt = getmetatable(x)
|
||||
return mt and mt.__index == Range
|
||||
end
|
||||
|
||||
---@param lpos string
|
||||
---@param rpos string
|
||||
---@return Range
|
||||
function Range.from_marks(lpos, rpos)
|
||||
local start = Pos.from_pos(lpos)
|
||||
local stop = Pos.from_pos(rpos)
|
||||
|
||||
---@type 'v'|'V'
|
||||
local mode
|
||||
if stop:is_col_max() then
|
||||
mode = 'V'
|
||||
else
|
||||
mode = 'v'
|
||||
end
|
||||
|
||||
return Range.new(start, stop, mode)
|
||||
end
|
||||
|
||||
---@param buf? number
|
||||
function Range.from_buf_text(buf)
|
||||
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||
local num_lines = vim.api.nvim_buf_line_count(buf)
|
||||
|
||||
local start = Pos.new(buf, 0, 0)
|
||||
local stop = Pos.new(buf, num_lines - 1, Pos.MAX_COL)
|
||||
return Range.new(start, stop, 'V')
|
||||
end
|
||||
|
||||
---@param buf? number
|
||||
---@param line number 0-based line index
|
||||
function Range.from_line(buf, line) return Range.from_lines(buf, line, line) end
|
||||
|
||||
---@param buf? number
|
||||
---@param start_line number 0-based line index
|
||||
---@param stop_line number 0-based line index
|
||||
function Range.from_lines(buf, start_line, stop_line)
|
||||
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||
if stop_line < 0 then
|
||||
local num_lines = vim.api.nvim_buf_line_count(buf)
|
||||
stop_line = num_lines + stop_line
|
||||
end
|
||||
return Range.new(Pos.new(buf, start_line, 0), Pos.new(buf, stop_line, Pos.MAX_COL), 'V')
|
||||
end
|
||||
|
||||
---@param text_obj string
|
||||
---@param opts? { buf?: number; contains_cursor?: boolean; pos?: Pos, user_defined?: boolean }
|
||||
---@return Range|nil
|
||||
function Range.from_text_object(text_obj, opts)
|
||||
opts = opts or {}
|
||||
if opts.buf == nil then opts.buf = vim.api.nvim_get_current_buf() end
|
||||
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
||||
if opts.user_defined == nil then opts.user_defined = false end
|
||||
|
||||
---@type "a" | "i"
|
||||
local selection_type = text_obj:sub(1, 1)
|
||||
local obj_type = text_obj:sub(#text_obj, #text_obj)
|
||||
local is_quote = vim.tbl_contains({ "'", '"', '`' }, obj_type)
|
||||
local cursor = Pos.from_pos '.'
|
||||
|
||||
-- Yank, then read '[ and '] to know the bounds:
|
||||
---@type { start: Pos; stop: Pos }
|
||||
local positions
|
||||
vim.api.nvim_buf_call(opts.buf, function()
|
||||
positions = State.run(0, function(s)
|
||||
s:track_winview()
|
||||
s:track_register '"'
|
||||
s:track_pos '.'
|
||||
s:track_pos "'["
|
||||
s:track_pos "']"
|
||||
|
||||
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
||||
|
||||
local null_pos = Pos.new(0, 0, 0, 0)
|
||||
null_pos:save_to_pos "'["
|
||||
null_pos:save_to_pos "']"
|
||||
|
||||
local prev_on_yank_enabled = on_yank_enabled
|
||||
on_yank_enabled = false
|
||||
vim.cmd {
|
||||
cmd = 'normal',
|
||||
bang = not opts.user_defined,
|
||||
args = { '""y' .. text_obj },
|
||||
mods = { silent = true },
|
||||
}
|
||||
on_yank_enabled = prev_on_yank_enabled
|
||||
|
||||
local start = Pos.from_pos "'["
|
||||
local stop = Pos.from_pos "']"
|
||||
|
||||
if
|
||||
-- I have no idea why, but when yanking `i"`, the stop-mark is
|
||||
-- placed on the ending quote. For other text-objects, the stop-
|
||||
-- mark is placed before the closing character.
|
||||
(is_quote and selection_type == 'i' and stop:char() == obj_type)
|
||||
-- *Sigh*, this also sometimes happens for `it` as well.
|
||||
or (text_obj == 'it' and stop:char() == '<')
|
||||
then
|
||||
stop = stop:next(-1) or stop
|
||||
end
|
||||
return { start = start, stop = stop }
|
||||
end)
|
||||
end)
|
||||
local start = positions.start
|
||||
local stop = positions.stop
|
||||
if start == stop and start.lnum == 0 and start.col == 0 and start.off == 0 then return nil end
|
||||
if opts.contains_cursor and not Range.new(start, stop):contains(cursor) then return nil end
|
||||
|
||||
if is_quote and selection_type == 'a' then
|
||||
start = start:find_next(1, obj_type) or start
|
||||
stop = stop:find_next(-1, obj_type) or stop
|
||||
end
|
||||
|
||||
return Range.new(start, stop)
|
||||
end
|
||||
|
||||
--- Get range information from the currently selected visual text.
|
||||
--- Note: from within a command mapping or an opfunc, use other specialized
|
||||
--- utilities, such as:
|
||||
--- * Range.from_cmd_args
|
||||
--- * Range.from_op_func
|
||||
function Range.from_vtext()
|
||||
local r = Range.from_marks('v', '.')
|
||||
if vim.fn.mode() == 'V' then r = r:to_linewise() end
|
||||
return r
|
||||
end
|
||||
|
||||
--- Get range information from the current text range being operated on
|
||||
--- as defined by an operator-pending function. Infers line-wise vs. char-wise
|
||||
--- based on the type, as given by the operator-pending function.
|
||||
---@param type 'line'|'char'|'block'
|
||||
function Range.from_op_func(type)
|
||||
if type == 'block' then error 'block motions not supported' end
|
||||
|
||||
local range = Range.from_marks("'[", "']")
|
||||
if type == 'line' then range = range:to_linewise() end
|
||||
return range
|
||||
end
|
||||
|
||||
--- Get range information from command arguments.
|
||||
---@param args unknown
|
||||
---@return Range|nil
|
||||
function Range.from_cmd_args(args)
|
||||
---@type 'v'|'V'
|
||||
local mode
|
||||
---@type nil|Pos
|
||||
local start
|
||||
local stop
|
||||
if args.range == 0 then
|
||||
return nil
|
||||
else
|
||||
start = Pos.from_pos "'<"
|
||||
stop = Pos.from_pos "'>"
|
||||
if stop:is_col_max() then
|
||||
mode = 'V'
|
||||
else
|
||||
mode = 'v'
|
||||
end
|
||||
end
|
||||
return Range.new(start, stop, mode)
|
||||
end
|
||||
|
||||
---
|
||||
function Range.find_nearest_brackets()
|
||||
local a = Range.from_text_object('a<', { contains_cursor = true })
|
||||
local b = Range.from_text_object('a[', { contains_cursor = true })
|
||||
local c = Range.from_text_object('a(', { contains_cursor = true })
|
||||
local d = Range.from_text_object('a{', { contains_cursor = true })
|
||||
return Range.smallest { a, b, c, d }
|
||||
end
|
||||
|
||||
function Range.find_nearest_quotes()
|
||||
local a = Range.from_text_object([[a']], { contains_cursor = true })
|
||||
if a ~= nil and a:is_empty() then a = nil end
|
||||
local b = Range.from_text_object([[a"]], { contains_cursor = true })
|
||||
if b ~= nil and b:is_empty() then b = nil end
|
||||
local c = Range.from_text_object([[a`]], { contains_cursor = true })
|
||||
if c ~= nil and c:is_empty() then c = nil end
|
||||
return Range.smallest { a, b, c }
|
||||
end
|
||||
|
||||
---@param ranges (Range|nil)[]
|
||||
function Range.smallest(ranges)
|
||||
---@type Range[]
|
||||
local new_ranges = {}
|
||||
for _, r in pairs(ranges) do
|
||||
if r ~= nil then table.insert(new_ranges, r) end
|
||||
end
|
||||
ranges = new_ranges
|
||||
if #ranges == 0 then return nil end
|
||||
|
||||
-- find smallest match
|
||||
local max_start = ranges[1].start
|
||||
local min_stop = ranges[1].stop
|
||||
local result = ranges[1]
|
||||
|
||||
for _, r in ipairs(ranges) do
|
||||
local start, stop = r.start, r.stop
|
||||
if start > max_start and stop < min_stop then
|
||||
max_start = start
|
||||
min_stop = stop
|
||||
result = r
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function Range:clone() return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode) end
|
||||
function Range:line_count()
|
||||
if self:is_empty() then return 0 end
|
||||
return self.stop.lnum - self.start.lnum + 1
|
||||
end
|
||||
|
||||
function Range:to_linewise()
|
||||
local r = self:clone()
|
||||
|
||||
r.mode = 'V'
|
||||
r.start.col = 0
|
||||
if r.stop ~= nil then r.stop.col = Pos.MAX_COL end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
function Range:is_empty() return self.stop == nil end
|
||||
|
||||
function Range:trim_start()
|
||||
if self:is_empty() then return end
|
||||
|
||||
local r = self:clone()
|
||||
while r.start:char():match '%s' do
|
||||
local next = r.start:next(1)
|
||||
if next == nil then break end
|
||||
r.start = next
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
function Range:trim_stop()
|
||||
if self:is_empty() then return end
|
||||
|
||||
local r = self:clone()
|
||||
while r.stop:char():match '%s' do
|
||||
local next = r.stop:next(-1)
|
||||
if next == nil then break end
|
||||
r.stop = next
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
---@param p Pos
|
||||
function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end
|
||||
|
||||
---@return string[]
|
||||
function Range:lines()
|
||||
if self:is_empty() then return {} end
|
||||
|
||||
local lines = {}
|
||||
for i = 0, self.stop.lnum - self.start.lnum do
|
||||
local line = self:line0(i)
|
||||
if line ~= nil then table.insert(lines, line.text()) end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Range:text() return vim.fn.join(self:lines(), '\n') end
|
||||
|
||||
---@param i number 1-based
|
||||
---@param j? number 1-based
|
||||
function Range:sub(i, j) return self:text():sub(i, j) end
|
||||
|
||||
---@param l number
|
||||
---@return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():Range; text: fun():string }|nil
|
||||
function Range:line0(l)
|
||||
if l < 0 then return self:line0(self:line_count() + l) end
|
||||
if l > self:line_count() then return end
|
||||
|
||||
local line = vim.api.nvim_buf_get_lines(self.start.buf, self.start.lnum + l, self.start.lnum + l + 1, false)[1]
|
||||
if line == nil then return end
|
||||
|
||||
local start = 0
|
||||
local stop = #line - 1
|
||||
if l == 0 then start = self.start.col end
|
||||
if l == self.stop.lnum - self.start.lnum then stop = self.stop.col end
|
||||
if stop == Pos.MAX_COL then stop = #line - 1 end
|
||||
local lnum = self.start.lnum + l
|
||||
|
||||
return {
|
||||
line = line,
|
||||
idx0 = { start = start, stop = stop },
|
||||
lnum = lnum,
|
||||
range = function()
|
||||
return Range.new(
|
||||
Pos.new(self.start.buf, lnum, start, self.start.off),
|
||||
Pos.new(self.start.buf, lnum, stop, self.stop.off),
|
||||
'v'
|
||||
)
|
||||
end,
|
||||
text = function() return line:sub(start + 1, stop + 1) end,
|
||||
}
|
||||
end
|
||||
|
||||
---@param replacement nil|string|string[]
|
||||
function Range:replace(replacement)
|
||||
if replacement == nil then replacement = {} end
|
||||
if type(replacement) == 'string' then replacement = vim.fn.split(replacement, '\n') end
|
||||
|
||||
local buf = self.start.buf
|
||||
-- convert to start-inclusive, stop-exclusive coordinates:
|
||||
local start_lnum, stop_lnum = self.start.lnum, (self.stop and self.stop.lnum or self.start.lnum) + 1
|
||||
local start_col, stop_col = self.start.col, (self.stop and self.stop.col or self.start.col) + 1
|
||||
|
||||
local replace_type = (self:is_empty() and 'insert') or (self.mode == 'v' and 'region') or 'lines'
|
||||
|
||||
---@param alnum number
|
||||
---@param acol number
|
||||
---@param blnum number
|
||||
---@param bcol number
|
||||
local function set_text(alnum, acol, blnum, bcol, repl)
|
||||
-- row indices are end-inclusive, and column indices are end-exclusive.
|
||||
vim.api.nvim_buf_set_text(buf, alnum, acol, blnum, bcol, repl)
|
||||
|
||||
local new_last_line_num = self.start.lnum + #replacement - 1
|
||||
local new_last_col = #(replacement[#replacement] or '')
|
||||
if new_last_line_num == start_lnum then new_last_col = new_last_col + start_col - 1 end
|
||||
|
||||
self.stop = Pos.new(buf, new_last_line_num, new_last_col)
|
||||
end
|
||||
|
||||
---@param alnum number
|
||||
---@param blnum number
|
||||
local function set_lines(alnum, blnum, repl)
|
||||
-- indexing is zero-based, end-exclusive
|
||||
vim.api.nvim_buf_set_lines(buf, alnum, blnum, false, repl)
|
||||
|
||||
if #repl == 0 then
|
||||
self.stop = nil
|
||||
else
|
||||
local new_last_line_num = start_lnum + #replacement - 1
|
||||
self.stop = Pos.new(self.start.buf, new_last_line_num, Pos.MAX_COL, self.stop.off)
|
||||
end
|
||||
self.mode = 'v'
|
||||
end
|
||||
|
||||
if replace_type == 'insert' then
|
||||
set_text(start_lnum, start_col, start_lnum, start_col, replacement)
|
||||
elseif replace_type == 'region' then
|
||||
-- Fixup the bounds:
|
||||
local last_line = vim.api.nvim_buf_get_lines(buf, stop_lnum - 1, stop_lnum, false)[1] or ''
|
||||
local max_col = #last_line
|
||||
set_text(start_lnum, start_col, stop_lnum - 1, math.min(stop_col, max_col), replacement)
|
||||
elseif replace_type == 'lines' then
|
||||
set_lines(start_lnum, stop_lnum, replacement)
|
||||
else
|
||||
error 'unreachable'
|
||||
end
|
||||
end
|
||||
|
||||
---@param amount number
|
||||
function Range:shrink(amount)
|
||||
local start = self.start
|
||||
local stop = self.stop
|
||||
if stop == nil then return self:clone() end
|
||||
|
||||
for _ = 1, amount do
|
||||
local next_start = start:next(1)
|
||||
local next_stop = stop:next(-1)
|
||||
if next_start == nil or next_stop == nil then return end
|
||||
start = next_start
|
||||
stop = next_stop
|
||||
if next_start > next_stop then break end
|
||||
end
|
||||
if start > stop then stop = nil end
|
||||
return Range.new(start, stop, self.mode)
|
||||
end
|
||||
|
||||
---@param amount number
|
||||
function Range:must_shrink(amount)
|
||||
local shrunk = self:shrink(amount)
|
||||
if shrunk == nil or shrunk:is_empty() then error 'error in Range:must_shrink: Range:shrink() returned nil' end
|
||||
return shrunk
|
||||
end
|
||||
|
||||
---@param left string
|
||||
---@param right string
|
||||
function Range:save_to_pos(left, right)
|
||||
if self:is_empty() then
|
||||
self.start:save_to_pos(left)
|
||||
self.start:save_to_pos(right)
|
||||
else
|
||||
self.start:save_to_pos(left)
|
||||
self.stop:save_to_pos(right)
|
||||
end
|
||||
end
|
||||
|
||||
---@param left string
|
||||
---@param right string
|
||||
function Range:save_to_marks(left, right)
|
||||
if self:is_empty() then
|
||||
self.start:save_to_mark(left)
|
||||
self.start:save_to_mark(right)
|
||||
else
|
||||
self.start:save_to_mark(left)
|
||||
self.stop:save_to_mark(right)
|
||||
end
|
||||
end
|
||||
|
||||
function Range:set_visual_selection()
|
||||
if self:is_empty() then return end
|
||||
|
||||
if vim.api.nvim_get_current_buf() ~= self.start.buf then vim.api.nvim_set_current_buf(self.start.buf) end
|
||||
|
||||
State.run(self.start.buf, function(s)
|
||||
s:track_mark 'a'
|
||||
s:track_mark 'b'
|
||||
|
||||
self.start:save_to_mark 'a'
|
||||
self.stop:save_to_mark 'b'
|
||||
local mode = self.mode
|
||||
|
||||
local normal_cmd_args = ''
|
||||
if vim.api.nvim_get_mode().mode == 'n' then normal_cmd_args = normal_cmd_args .. mode end
|
||||
normal_cmd_args = normal_cmd_args .. '`ao`b'
|
||||
vim.cmd { cmd = 'normal', args = { normal_cmd_args }, bang = true }
|
||||
|
||||
return nil
|
||||
end)
|
||||
end
|
||||
|
||||
---@param group string
|
||||
---@param opts? { timeout?: number, priority?: number, on_macro?: boolean }
|
||||
function Range:highlight(group, opts)
|
||||
if self:is_empty() then return end
|
||||
|
||||
opts = opts or { on_macro = false }
|
||||
if opts.on_macro == nil then opts.on_macro = false end
|
||||
|
||||
local in_macro = vim.fn.reg_executing() ~= ''
|
||||
if not opts.on_macro and in_macro then return { clear = function() end } end
|
||||
|
||||
local ns = vim.api.nvim_create_namespace ''
|
||||
State.run(self.start.buf, function(s)
|
||||
if not in_macro then s:track_winview() end
|
||||
|
||||
vim.highlight.range(
|
||||
self.start.buf,
|
||||
ns,
|
||||
group,
|
||||
{ self.start.lnum, self.start.col },
|
||||
{ self.stop.lnum, self.stop.col },
|
||||
{
|
||||
inclusive = true,
|
||||
priority = opts.priority,
|
||||
regtype = self.mode,
|
||||
}
|
||||
)
|
||||
|
||||
return nil
|
||||
end)
|
||||
vim.cmd.redraw()
|
||||
|
||||
local function clear()
|
||||
vim.api.nvim_buf_clear_namespace(self.start.buf, ns, self.start.lnum, self.stop.lnum + 1)
|
||||
vim.cmd.redraw()
|
||||
end
|
||||
if opts.timeout ~= nil then vim.defer_fn(clear, opts.timeout) end
|
||||
|
||||
return { ns = ns, clear = clear }
|
||||
end
|
||||
|
||||
return Range
|
||||
@@ -1,61 +0,0 @@
|
||||
local M = {}
|
||||
|
||||
local function _normal(cmd) vim.cmd { cmd = 'normal', args = { cmd }, bang = true } end
|
||||
|
||||
M.native_repeat = function() _normal '.' end
|
||||
M.native_undo = function() _normal 'u' end
|
||||
|
||||
local function update_ts() vim.b.tt_changedtick = vim.b.changedtick end
|
||||
|
||||
---@param cmd? string|fun():unknown
|
||||
function M.set(cmd)
|
||||
update_ts()
|
||||
if cmd ~= nil then vim.b.tt_repeatcmd = cmd end
|
||||
end
|
||||
|
||||
local function tt_was_last_repeatable()
|
||||
local ts, tt_ts = vim.b.changedtick, vim.b.tt_changedtick
|
||||
return tt_ts ~= nil and ts <= tt_ts
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param cmd string|fun():T
|
||||
---@return T
|
||||
function M.run(cmd)
|
||||
M.set(cmd)
|
||||
local result = cmd()
|
||||
update_ts()
|
||||
return result
|
||||
end
|
||||
|
||||
function M.do_repeat()
|
||||
local tt_cmd = vim.b.tt_repeatcmd
|
||||
if not tt_was_last_repeatable() or (type(tt_cmd) ~= 'function' and type(tt_cmd) ~= 'string') then
|
||||
return M.native_repeat()
|
||||
end
|
||||
|
||||
-- execute the cached command:
|
||||
local count = vim.api.nvim_get_vvar 'count1'
|
||||
if type(tt_cmd) == 'string' then
|
||||
_normal(count .. tt_cmd --[[@as string]])
|
||||
else
|
||||
local last_return
|
||||
for _ = 1, count do
|
||||
last_return = M.run(tt_cmd --[[@as fun():any]])
|
||||
end
|
||||
return last_return
|
||||
end
|
||||
end
|
||||
|
||||
function M.undo()
|
||||
local tt_was_last_repeatable_before_undo = tt_was_last_repeatable()
|
||||
M.native_undo()
|
||||
if tt_was_last_repeatable_before_undo then update_ts() end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
vim.keymap.set('n', '.', M.do_repeat)
|
||||
vim.keymap.set('n', 'u', M.undo)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,90 +0,0 @@
|
||||
---@class State
|
||||
---@field buf number
|
||||
---@field registers table
|
||||
---@field marks table
|
||||
---@field positions table
|
||||
---@field keymaps { mode: string; lhs: any, rhs: any, buffer?: number }[]
|
||||
---@field global_options table<string, any>
|
||||
---@field win_view vim.fn.winsaveview.ret|nil
|
||||
local State = {}
|
||||
|
||||
---@param buf number
|
||||
---@return State
|
||||
function State.new(buf)
|
||||
if buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||
local s = { buf = buf, registers = {}, marks = {}, positions = {}, keymaps = {}, global_options = {} }
|
||||
setmetatable(s, { __index = State })
|
||||
return s
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param buf number
|
||||
---@param f fun(s: State):T
|
||||
---@return T
|
||||
function State.run(buf, f)
|
||||
local s = State.new(buf)
|
||||
local ok, result = pcall(f, s)
|
||||
s:restore()
|
||||
if not ok then error(result) end
|
||||
return result
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param f fun(s: State, callback: fun(): any):any
|
||||
---@param callback fun():any
|
||||
function State.run_async(buf, f, callback)
|
||||
local s = State.new(buf)
|
||||
f(s, function()
|
||||
s:restore()
|
||||
callback()
|
||||
end)
|
||||
end
|
||||
|
||||
function State:track_keymap(mode, lhs)
|
||||
local old =
|
||||
-- Look up the mapping in buffer-local maps:
|
||||
vim.iter(vim.api.nvim_buf_get_keymap(self.buf, mode)):find(function(map) return map.lhs == lhs end)
|
||||
-- Look up the mapping in global maps:
|
||||
or vim.iter(vim.api.nvim_get_keymap(mode)):find(function(map) return map.lhs == lhs end)
|
||||
|
||||
-- Did we find a mapping?
|
||||
if old == nil then return end
|
||||
|
||||
-- Track it:
|
||||
table.insert(self.keymaps, { mode = mode, lhs = lhs, rhs = old.rhs or old.callback, buffer = old.buffer })
|
||||
end
|
||||
|
||||
---@param reg string
|
||||
function State:track_register(reg) self.registers[reg] = vim.fn.getreg(reg) end
|
||||
|
||||
---@param mark string
|
||||
function State:track_mark(mark) self.marks[mark] = vim.api.nvim_buf_get_mark(self.buf, mark) end
|
||||
|
||||
---@param pos string
|
||||
function State:track_pos(pos) self.positions[pos] = vim.fn.getpos(pos) end
|
||||
|
||||
---@param nm string
|
||||
function State:track_global_option(nm) self.global_options[nm] = vim.go[nm] end
|
||||
|
||||
function State:track_winview() self.win_view = vim.fn.winsaveview() end
|
||||
|
||||
function State:restore()
|
||||
for reg, val in pairs(self.registers) do
|
||||
vim.fn.setreg(reg, val)
|
||||
end
|
||||
for mark, val in pairs(self.marks) do
|
||||
vim.api.nvim_buf_set_mark(self.buf, mark, val[1], val[2], {})
|
||||
end
|
||||
for pos, val in pairs(self.positions) do
|
||||
vim.fn.setpos(pos, val)
|
||||
end
|
||||
for _, map in ipairs(self.keymaps) do
|
||||
vim.keymap.set(map.mode, map.lhs, map.rhs, { buffer = map.buffer })
|
||||
end
|
||||
for nm, val in pairs(self.global_options) do
|
||||
vim.go[nm] = val
|
||||
end
|
||||
if self.win_view ~= nil then vim.fn.winrestview(self.win_view) end
|
||||
end
|
||||
|
||||
return State
|
||||
134
lua/u/utils.lua
134
lua/u/utils.lua
@@ -1,134 +0,0 @@
|
||||
local M = {}
|
||||
|
||||
--
|
||||
-- Types
|
||||
--
|
||||
|
||||
---@alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
|
||||
---@alias KeyMaps table<string, fun(): any | string> }
|
||||
---@alias CmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any; info: Range|nil }
|
||||
|
||||
--- A utility for creating user commands that also pre-computes useful information
|
||||
--- and attaches it to the arguments.
|
||||
---
|
||||
--- ```lua
|
||||
--- -- Example:
|
||||
--- ucmd('MyCmd', function(args)
|
||||
--- -- print the visually selected text:
|
||||
--- vim.print(args.info:text())
|
||||
--- -- or get the vtext as an array of lines:
|
||||
--- vim.print(args.info:lines())
|
||||
--- end, { nargs = '*', range = true })
|
||||
--- ```
|
||||
---@param name string
|
||||
---@param cmd string | fun(args: CmdArgs): any
|
||||
---@param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
|
||||
function M.ucmd(name, cmd, opts)
|
||||
local Range = require 'u.range'
|
||||
|
||||
opts = opts or {}
|
||||
local cmd2 = cmd
|
||||
if type(cmd) == 'function' then
|
||||
cmd2 = function(args)
|
||||
args.info = Range.from_cmd_args(args)
|
||||
return cmd(args)
|
||||
end
|
||||
end
|
||||
vim.api.nvim_create_user_command(name, cmd2, opts or {})
|
||||
end
|
||||
|
||||
---@param key_seq string
|
||||
---@param fn fun(key_seq: string):Range|Pos|nil
|
||||
---@param opts? { buffer: number|nil }
|
||||
function M.define_text_object(key_seq, fn, opts)
|
||||
local Range = require 'u.range'
|
||||
local Pos = require 'u.pos'
|
||||
|
||||
if opts ~= nil and opts.buffer == 0 then opts.buffer = vim.api.nvim_get_current_buf() end
|
||||
|
||||
local function handle_visual()
|
||||
local range_or_pos = fn(key_seq)
|
||||
if range_or_pos == nil then return end
|
||||
if Range.is(range_or_pos) and range_or_pos:is_empty() then range_or_pos = range_or_pos.start end
|
||||
|
||||
if Range.is(range_or_pos) then
|
||||
local range = range_or_pos --[[@as Range]]
|
||||
range:set_visual_selection()
|
||||
else
|
||||
vim.cmd { cmd = 'normal', args = { '<Esc>' }, bang = true }
|
||||
end
|
||||
end
|
||||
vim.keymap.set({ 'x' }, key_seq, handle_visual, opts and { buffer = opts.buffer } or nil)
|
||||
|
||||
local function handle_normal()
|
||||
local State = require 'u.state'
|
||||
|
||||
-- enter visual mode:
|
||||
vim.cmd { cmd = 'normal', args = { 'v' }, bang = true }
|
||||
|
||||
local range_or_pos = fn(key_seq)
|
||||
if range_or_pos == nil then return end
|
||||
if Range.is(range_or_pos) and range_or_pos:is_empty() then range_or_pos = range_or_pos.start end
|
||||
|
||||
if Range.is(range_or_pos) then
|
||||
range_or_pos:set_visual_selection()
|
||||
elseif Pos.is(range_or_pos) then
|
||||
local p = range_or_pos --[[@as Pos]]
|
||||
State.run(0, function(s)
|
||||
s:track_global_option 'eventignore'
|
||||
vim.go.eventignore = 'all'
|
||||
|
||||
-- insert a single space, so we can select it:
|
||||
vim.api.nvim_buf_set_text(0, p.lnum, p.col, p.lnum, p.col, { ' ' })
|
||||
-- select the space:
|
||||
Range.new(p, p, 'v'):set_visual_selection()
|
||||
end)
|
||||
end
|
||||
end
|
||||
vim.keymap.set({ 'o' }, key_seq, handle_normal, opts and { buffer = opts.buffer } or nil)
|
||||
end
|
||||
|
||||
---@type fun(): nil|(fun():any)
|
||||
local __U__RepeatableOpFunc_rhs = nil
|
||||
|
||||
--- This is the global utility function used for operatorfunc
|
||||
--- in repeatablemap
|
||||
---@type nil|fun(range: Range): fun():any|nil
|
||||
-- selene: allow(unused_variable)
|
||||
function __U__RepeatableOpFunc()
|
||||
if __U__RepeatableOpFunc_rhs ~= nil then __U__RepeatableOpFunc_rhs() end
|
||||
end
|
||||
|
||||
function M.repeatablemap(mode, lhs, rhs, opts)
|
||||
vim.keymap.set(mode, lhs, function()
|
||||
__U__RepeatableOpFunc_rhs = rhs
|
||||
vim.o.operatorfunc = 'v:lua.__U__RepeatableOpFunc'
|
||||
return 'g@ '
|
||||
end, vim.tbl_extend('force', opts or {}, { expr = true }))
|
||||
end
|
||||
|
||||
function M.get_editor_dimensions()
|
||||
local w = 0
|
||||
local h = 0
|
||||
local tabnr = vim.api.nvim_get_current_tabpage()
|
||||
for _, winid in ipairs(vim.api.nvim_list_wins()) do
|
||||
local tabpage = vim.api.nvim_win_get_tabpage(winid)
|
||||
if tabpage == tabnr then
|
||||
local pos = vim.api.nvim_win_get_position(winid)
|
||||
local r, c = pos[1], pos[2]
|
||||
local win_w = vim.api.nvim_win_get_width(winid)
|
||||
local win_h = vim.api.nvim_win_get_height(winid)
|
||||
local right = c + win_w
|
||||
local bottom = r + win_h
|
||||
if right > w then w = right end
|
||||
if bottom > h then h = bottom end
|
||||
end
|
||||
end
|
||||
if w == 0 or h == 0 then
|
||||
w = vim.api.nvim_win_get_width(0)
|
||||
h = vim.api.nvim_win_get_height(0)
|
||||
end
|
||||
return { width = w, height = h }
|
||||
end
|
||||
|
||||
return M
|
||||
109
mise.toml
Normal file
109
mise.toml
Normal file
@@ -0,0 +1,109 @@
|
||||
################################################################################
|
||||
## Tool Alias
|
||||
################################################################################
|
||||
|
||||
################################################################################
|
||||
## Tools
|
||||
################################################################################
|
||||
|
||||
[tools]
|
||||
# nvimv needs jq:
|
||||
jq = "1.8.1"
|
||||
"asdf:mise-plugins/mise-lua" = "5.1.5"
|
||||
# Since we have busted configured to look for "nvim", have a "default" version
|
||||
# installed. In the tests, we will override with `eval $(nvimv env VERSION)`,
|
||||
# but to avoid having to litter a bunch of commands with that environment
|
||||
# initialization, this just makes things simpler:
|
||||
neovim = "0.12.1"
|
||||
"http:nvimv" = { version = "latest", url = "https://raw.githubusercontent.com/jrop/nvimv/refs/heads/main/nvimv" }
|
||||
stylua = "2.3.1"
|
||||
cargo-binstall = "1.18.1"
|
||||
"cargo:emmylua_ls" = { version = "0.20.0", depends=["cargo-binstall"] }
|
||||
"cargo:emmylua_check" = { version = "0.20.0", depends=["cargo-binstall"] }
|
||||
|
||||
################################################################################
|
||||
# Env
|
||||
################################################################################
|
||||
|
||||
[env]
|
||||
ASDF_LUA_LUAROCKS_VERSION = "3.12.2"
|
||||
_.source = { path = "./scripts/env.sh", tools = true }
|
||||
|
||||
################################################################################
|
||||
# Tasks
|
||||
################################################################################
|
||||
|
||||
[tasks]
|
||||
lint = "emmylua_check --ignore '.prefix/**/*.*' ."
|
||||
fmt = "stylua ."
|
||||
"fmt:check" = "stylua --check ."
|
||||
"test:prepare" = '''
|
||||
# Install Lua test dependencies (busted, etc.):
|
||||
luarocks test --prepare
|
||||
echo
|
||||
|
||||
# Check that the nightly version of Neovim is not more than a day old:
|
||||
if find .prefix -path '**/nightly/**/nvim' -mtime -1 2>/dev/null | grep -q .; then
|
||||
echo "Neovim Nightly is up-to-date"
|
||||
else
|
||||
if nvimv ls | grep nightly >/dev/null; then
|
||||
nvimv upgrade nightly
|
||||
else
|
||||
nvimv install nightly
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
'''
|
||||
|
||||
[tasks."test:version:no-prep"]
|
||||
hide = true
|
||||
usage = '''
|
||||
arg "<version>" help="The version of Neovim to test with"
|
||||
'''
|
||||
run = '''
|
||||
echo
|
||||
echo -----------------------------
|
||||
echo -----------------------------
|
||||
echo Neovim version=$usage_version
|
||||
echo -----------------------------
|
||||
echo -----------------------------
|
||||
echo
|
||||
nvimv install $usage_version
|
||||
eval $(nvimv env $usage_version)
|
||||
busted --verbose
|
||||
'''
|
||||
|
||||
[tasks."test:version"]
|
||||
depends = ["test:prepare"]
|
||||
usage = '''
|
||||
arg "<version>" help="The version of Neovim to test with"
|
||||
'''
|
||||
run = 'mise test:version:no-prep $usage_version'
|
||||
|
||||
[tasks."test"]
|
||||
run = 'mise test:version 0.12.1'
|
||||
|
||||
[tasks."test:all"]
|
||||
depends = ["test:prepare"]
|
||||
run = '''
|
||||
VERSIONS="0.11.5 0.12.1 nightly"
|
||||
for v in $VERSIONS; do
|
||||
mise test:version:no-prep $v
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks."test:coverage"]
|
||||
depends = ["test:prepare"]
|
||||
run = '''
|
||||
rm -f ./luacov.*.*
|
||||
busted --coverage --verbose
|
||||
luacov
|
||||
awk '/^Summary$/{flag=1;next} flag{print}' luacov.report.out
|
||||
'''
|
||||
|
||||
[tasks."ci"]
|
||||
run = '''
|
||||
mise run fmt:check
|
||||
mise run lint
|
||||
mise run test:all
|
||||
'''
|
||||
6
scripts/env.sh
Normal file
6
scripts/env.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PREFIX="$(pwd)/.prefix"
|
||||
export VIMRUNTIME="$(nvim -u NORC --headless +'echo $VIMRUNTIME' +'quitall' 2>&1)"
|
||||
|
||||
eval $(luarocks path)
|
||||
43
scripts/make-artifact-tag.sh
Executable file
43
scripts/make-artifact-tag.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# After tagging a release (say, v1.2.3), use this script to publish
|
||||
# lua/u.lua in an artifact branch: artifact-v1.2.3:init.lua. To do so,
|
||||
# invoke the script from the root of the repo like so:
|
||||
#
|
||||
# ./scripts/make-artifact-tag.sh v1.2.3
|
||||
#
|
||||
# This will create a temporary orphan branch, tag it and immediately delete the
|
||||
# branch.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <TAG>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="$1"
|
||||
if [[ "$(echo "$TAG" | awk '/^v[0-9]+\.[0-9]+\.[0-9]+$/ { print "YES" }')" != "YES" ]]; then
|
||||
echo "Invalid tag name: expected 'vX.Y.Z'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARTIFACT_TAG="artifact-$TAG"
|
||||
EXISTING_ARTIFACT_TAG=$(git tag -l "$ARTIFACT_TAG")
|
||||
|
||||
if [[ "$ARTIFACT_TAG" = "$EXISTING_ARTIFACT_TAG" ]]; then
|
||||
echo "Artifact tag already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git worktree add --orphan -b "$ARTIFACT_TAG" "$ARTIFACT_TAG"
|
||||
git cat-file -p "$TAG":lua/u.lua > "$ARTIFACT_TAG"/init.lua
|
||||
|
||||
pushd "$ARTIFACT_TAG"
|
||||
git add --all
|
||||
git commit --message "$ARTIFACT_TAG"
|
||||
git tag "$ARTIFACT_TAG"
|
||||
popd
|
||||
|
||||
git worktree remove -f "$ARTIFACT_TAG"
|
||||
git branch -D "$ARTIFACT_TAG"
|
||||
@@ -1,4 +0,0 @@
|
||||
std = "vim"
|
||||
|
||||
[lints]
|
||||
multiple_statements = "allow"
|
||||
@@ -1,30 +0,0 @@
|
||||
local Buffer = require 'u.buffer'
|
||||
local withbuf = loadfile './spec/withbuf.lua'()
|
||||
|
||||
describe('Buffer', function()
|
||||
it('should replace all lines', function()
|
||||
withbuf({}, function()
|
||||
local buf = Buffer.from_nr()
|
||||
buf:all():replace 'bleh'
|
||||
local actual_lines = vim.api.nvim_buf_get_lines(buf.buf, 0, -1, false)
|
||||
assert.are.same({ 'bleh' }, actual_lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('should replace all but first and last lines', function()
|
||||
withbuf({
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
}, function()
|
||||
local buf = Buffer.from_nr()
|
||||
buf:lines(1, -2):replace 'too'
|
||||
local actual_lines = vim.api.nvim_buf_get_lines(buf.buf, 0, -1, false)
|
||||
assert.are.same({
|
||||
'one',
|
||||
'too',
|
||||
'three',
|
||||
}, actual_lines)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
@@ -1,29 +0,0 @@
|
||||
local CodeWriter = require 'u.codewriter'
|
||||
|
||||
describe('CodeWriter', function()
|
||||
it('should write with indentation', function()
|
||||
local cw = CodeWriter.new()
|
||||
cw:write '{'
|
||||
cw:indent(function(cw2) cw2:write 'x: 123' end)
|
||||
cw:write '}'
|
||||
|
||||
assert.are.same(cw.lines, { '{', ' x: 123', '}' })
|
||||
end)
|
||||
|
||||
it('should keep relative indentation', function()
|
||||
local cw = CodeWriter.new()
|
||||
cw:write '{'
|
||||
cw:indent(function(cw2)
|
||||
cw2:write 'x: 123'
|
||||
cw2:write ' y: 123'
|
||||
end)
|
||||
cw:write '}'
|
||||
|
||||
assert.are.same(cw.lines, {
|
||||
'{',
|
||||
' x: 123',
|
||||
' y: 123',
|
||||
'}',
|
||||
})
|
||||
end)
|
||||
end)
|
||||
@@ -1,69 +0,0 @@
|
||||
local Pos = require 'u.pos'
|
||||
local withbuf = loadfile './spec/withbuf.lua'()
|
||||
|
||||
describe('Pos', function()
|
||||
it('get a char from a given position', function()
|
||||
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
||||
assert.are.same('a', Pos.new(nil, 0, 0):char())
|
||||
assert.are.same('d', Pos.new(nil, 0, 2):char())
|
||||
assert.are.same('f', Pos.new(nil, 0, 3):char())
|
||||
assert.are.same('a', Pos.new(nil, 2, 0):char())
|
||||
assert.are.same('', Pos.new(nil, 3, 0):char())
|
||||
assert.are.same('o', Pos.new(nil, 4, 2):char())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('comparison operators', function()
|
||||
local a = Pos.new(0, 0, 0, 0)
|
||||
local b = Pos.new(0, 1, 0, 0)
|
||||
assert.are.same(a == a, true)
|
||||
assert.are.same(a < b, true)
|
||||
end)
|
||||
|
||||
it('get the next position', function()
|
||||
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
||||
-- line 1: a => s
|
||||
assert.are.same(Pos.new(nil, 0, 1), Pos.new(nil, 0, 0):next())
|
||||
-- line 1: d => f
|
||||
assert.are.same(Pos.new(nil, 0, 3), Pos.new(nil, 0, 2):next())
|
||||
-- line 1 => 2
|
||||
assert.are.same(Pos.new(nil, 1, 0), Pos.new(nil, 0, 3):next())
|
||||
-- line 3 => 4
|
||||
assert.are.same(Pos.new(nil, 3, 0), Pos.new(nil, 2, 0):next())
|
||||
-- line 4 => 5
|
||||
assert.are.same(Pos.new(nil, 4, 0), Pos.new(nil, 3, 0):next())
|
||||
-- end returns nil
|
||||
assert.are.same(nil, Pos.new(nil, 4, 2):next())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get the previous position', function()
|
||||
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
||||
-- line 1: s => a
|
||||
assert.are.same(Pos.new(nil, 0, 0), Pos.new(nil, 0, 1):next(-1))
|
||||
-- line 1: f => d
|
||||
assert.are.same(Pos.new(nil, 0, 2), Pos.new(nil, 0, 3):next(-1))
|
||||
-- line 2 => 1
|
||||
assert.are.same(Pos.new(nil, 0, 3), Pos.new(nil, 1, 0):next(-1))
|
||||
-- line 4 => 3
|
||||
assert.are.same(Pos.new(nil, 2, 0), Pos.new(nil, 3, 0):next(-1))
|
||||
-- line 5 => 4
|
||||
assert.are.same(Pos.new(nil, 3, 0), Pos.new(nil, 4, 0):next(-1))
|
||||
-- beginning returns nil
|
||||
assert.are.same(nil, Pos.new(nil, 0, 0):next(-1))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('find matching brackets', function()
|
||||
withbuf({ 'asdf ({} def <[{}]>) ;lkj' }, function()
|
||||
-- outer parens are matched:
|
||||
assert.are.same(Pos.new(nil, 0, 19), Pos.new(nil, 0, 5):find_match())
|
||||
-- outer parens are matched (backward):
|
||||
assert.are.same(Pos.new(nil, 0, 5), Pos.new(nil, 0, 19):find_match())
|
||||
-- no potential match returns nil
|
||||
assert.are.same(nil, Pos.new(nil, 0, 0):find_match())
|
||||
-- watchdog expires before an otherwise valid match is found:
|
||||
assert.are.same(nil, Pos.new(nil, 0, 5):find_match(2))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
@@ -1,560 +0,0 @@
|
||||
local Range = require 'u.range'
|
||||
local Pos = require 'u.pos'
|
||||
local withbuf = loadfile './spec/withbuf.lua'()
|
||||
|
||||
describe('Range', function()
|
||||
it('get text in buffer', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.from_buf_text()
|
||||
local lines = range:lines()
|
||||
assert.are.same({
|
||||
'line one',
|
||||
'and line two',
|
||||
}, lines)
|
||||
|
||||
local text = range:text()
|
||||
assert.are.same('line one\nand line two', text)
|
||||
end)
|
||||
|
||||
withbuf({}, function()
|
||||
local range = Range.from_buf_text()
|
||||
local lines = range:lines()
|
||||
assert.are.same({ '' }, lines)
|
||||
|
||||
local text = range:text()
|
||||
assert.are.same('', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get from positions: v in single line', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v')
|
||||
local lines = range:lines()
|
||||
assert.are.same({ 'ine' }, lines)
|
||||
|
||||
local text = range:text()
|
||||
assert.are.same('ine', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get from positions: v across multiple lines', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 4), 'v')
|
||||
local lines = range:lines()
|
||||
assert.are.same({ 'quick brown fox', 'jumps' }, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get from positions: V', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, Pos.MAX_COL), 'V')
|
||||
local lines = range:lines()
|
||||
assert.are.same({ 'line one' }, lines)
|
||||
|
||||
local text = range:text()
|
||||
assert.are.same('line one', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get from positions: V across multiple lines', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.new(Pos.new(nil, 1, 0), Pos.new(nil, 2, Pos.MAX_COL), 'V')
|
||||
local lines = range:lines()
|
||||
assert.are.same({ 'the quick brown fox', 'jumps over a lazy dog' }, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get from line', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.from_line(nil, 0)
|
||||
local lines = range:lines()
|
||||
assert.are.same({ 'line one' }, lines)
|
||||
|
||||
local text = range:text()
|
||||
assert.are.same('line one', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('get from lines', function()
|
||||
withbuf({ 'line one', 'and line two', 'and line 3' }, function()
|
||||
local range = Range.from_lines(nil, 0, 1)
|
||||
local lines = range:lines()
|
||||
assert.are.same({ 'line one', 'and line two' }, lines)
|
||||
|
||||
local text = range:text()
|
||||
assert.are.same('line one\nand line two', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace within line', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 8), 'v')
|
||||
range:replace 'quack'
|
||||
|
||||
local text = Range.from_line(nil, 1):text()
|
||||
assert.are.same('the quack brown fox', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('delete within line', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 9), 'v')
|
||||
range:replace ''
|
||||
|
||||
local text = Range.from_line(nil, 1):text()
|
||||
assert.are.same('the brown fox', text)
|
||||
end)
|
||||
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 9), 'v')
|
||||
range:replace(nil)
|
||||
|
||||
local text = Range.from_line(nil, 1):text()
|
||||
assert.are.same('the brown fox', text)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace across multiple lines: v', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 4), 'v')
|
||||
range:replace 'plane flew'
|
||||
|
||||
local lines = Range.from_buf_text():lines()
|
||||
assert.are.same({
|
||||
'pre line',
|
||||
'the plane flew over a lazy dog',
|
||||
'post line',
|
||||
}, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace a line', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.from_line(nil, 1)
|
||||
range:replace 'the rabbit'
|
||||
|
||||
local lines = Range.from_buf_text():lines()
|
||||
assert.are.same({
|
||||
'pre line',
|
||||
'the rabbit',
|
||||
'jumps over a lazy dog',
|
||||
'post line',
|
||||
}, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace multiple lines', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.from_lines(nil, 1, 2)
|
||||
range:replace 'the rabbit'
|
||||
|
||||
local lines = Range.from_buf_text():lines()
|
||||
assert.are.same({
|
||||
'pre line',
|
||||
'the rabbit',
|
||||
'post line',
|
||||
}, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('delete single line', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.from_line(nil, 1)
|
||||
range:replace(nil) -- delete lines
|
||||
|
||||
local lines = Range.from_buf_text():lines()
|
||||
assert.are.same({
|
||||
'pre line',
|
||||
'jumps over a lazy dog',
|
||||
'post line',
|
||||
}, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('delete multiple lines', function()
|
||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||
local range = Range.from_lines(nil, 1, 2)
|
||||
range:replace(nil) -- delete lines
|
||||
|
||||
local lines = Range.from_buf_text():lines()
|
||||
assert.are.same({
|
||||
'pre line',
|
||||
'post line',
|
||||
}, lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('text object: word', function()
|
||||
withbuf({ 'the quick brown fox' }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
assert.are.same('quick ', Range.from_text_object('aw'):text())
|
||||
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
assert.are.same('quick', Range.from_text_object('iw'):text())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('text object: quote', function()
|
||||
withbuf({ [[the "quick" brown fox]] }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
assert.are.same('"quick"', Range.from_text_object('a"'):text())
|
||||
|
||||
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
||||
assert.are.same('quick', Range.from_text_object('i"'):text())
|
||||
end)
|
||||
|
||||
withbuf({ [[the 'quick' brown fox]] }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
assert.are.same("'quick'", Range.from_text_object([[a']]):text())
|
||||
|
||||
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
||||
assert.are.same('quick', Range.from_text_object([[i']]):text())
|
||||
end)
|
||||
|
||||
withbuf({ [[the `quick` brown fox]] }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
assert.are.same('`quick`', Range.from_text_object([[a`]]):text())
|
||||
|
||||
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
||||
assert.are.same('quick', Range.from_text_object([[i`]]):text())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('text object: block', function()
|
||||
withbuf({ 'this is a {', 'block', '} here' }, function()
|
||||
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
||||
assert.are.same('{\nblock\n}', Range.from_text_object('a{'):text())
|
||||
|
||||
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
||||
assert.are.same('block', Range.from_text_object('i{'):text())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('text object: restores cursor position', function()
|
||||
withbuf({ 'this is a {block} here' }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 13, 0 })
|
||||
assert.are.same('{block}', Range.from_text_object('a{'):text())
|
||||
assert.are.same(vim.api.nvim_win_get_cursor(0), { 1, 12 })
|
||||
end)
|
||||
end)
|
||||
|
||||
it('should get nearest block', function()
|
||||
withbuf({
|
||||
'this is a {',
|
||||
'block',
|
||||
'} here',
|
||||
}, function()
|
||||
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
||||
assert.are.same('{\nblock\n}', Range.find_nearest_brackets():text())
|
||||
end)
|
||||
|
||||
withbuf({
|
||||
'this is a {',
|
||||
'(block)',
|
||||
'} here',
|
||||
}, function()
|
||||
vim.fn.setpos('.', { 0, 2, 3, 0 })
|
||||
assert.are.same('(block)', Range.find_nearest_brackets():text())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('line0', function()
|
||||
withbuf({
|
||||
'this is a {',
|
||||
'block',
|
||||
'} here',
|
||||
}, function()
|
||||
local range = Range.new(Pos.new(0, 0, 5), Pos.new(0, 1, 4), 'v')
|
||||
local lfirst = range:line0(0)
|
||||
assert.are.same(5, lfirst.idx0.start)
|
||||
assert.are.same(10, lfirst.idx0.stop)
|
||||
assert.are.same(0, lfirst.lnum)
|
||||
assert.are.same('is a {', lfirst.text())
|
||||
assert.are.same('is a {', lfirst.range():text())
|
||||
assert.are.same(Pos.new(0, 0, 5), lfirst.range().start)
|
||||
assert.are.same(Pos.new(0, 0, 10), lfirst.range().stop)
|
||||
assert.are.same('block', range:line0(1).text())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('from_marks', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local a = Pos.new(nil, 0, 0)
|
||||
local b = Pos.new(nil, 1, 1)
|
||||
a:save_to_pos "'["
|
||||
b:save_to_pos "']"
|
||||
|
||||
local range = Range.from_marks("'[", "']")
|
||||
assert.are.same(range.start, a)
|
||||
assert.are.same(range.stop, b)
|
||||
assert.are.same(range.mode, 'v')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('from_vtext', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 3, 0 }) -- cursor at position (1, 3)
|
||||
vim.cmd.normal 'v' -- enter visual mode
|
||||
vim.cmd.normal 'l' -- select one character to the right
|
||||
local range = Range.from_vtext()
|
||||
assert.are.same(range.start, Pos.new(nil, 0, 2))
|
||||
assert.are.same(range.stop, Pos.new(nil, 0, 3))
|
||||
assert.are.same(range.mode, 'v')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('from_op_func', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local a = Pos.new(nil, 0, 0)
|
||||
local b = Pos.new(nil, 1, 1)
|
||||
a:save_to_pos "'["
|
||||
b:save_to_pos "']"
|
||||
|
||||
local range = Range.from_op_func 'char'
|
||||
assert.are.same(range.start, a)
|
||||
assert.are.same(range.stop, b)
|
||||
assert.are.same(range.mode, 'v')
|
||||
|
||||
range = Range.from_op_func 'line'
|
||||
assert.are.same(range.start, a)
|
||||
assert.are.same(range.stop, Pos.new(nil, 1, Pos.MAX_COL))
|
||||
assert.are.same(range.mode, 'V')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('from_cmd_args', function()
|
||||
local args = { range = 1 }
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local a = Pos.new(nil, 0, 0)
|
||||
local b = Pos.new(nil, 1, 1)
|
||||
a:save_to_pos "'<"
|
||||
b:save_to_pos "'>"
|
||||
|
||||
local range = Range.from_cmd_args(args)
|
||||
assert.are.same(range.start, a)
|
||||
assert.are.same(range.stop, b)
|
||||
assert.are.same(range.mode, 'v')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('find_nearest_quotes', function()
|
||||
withbuf({ [[the "quick" brown fox]] }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
local range = Range.find_nearest_quotes()
|
||||
assert.are.same(range.start, Pos.new(nil, 0, 4))
|
||||
assert.are.same(range.stop, Pos.new(nil, 0, 10))
|
||||
end)
|
||||
|
||||
withbuf({ [[the 'quick' brown fox]] }, function()
|
||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||
local range = Range.find_nearest_quotes()
|
||||
assert.are.same(range.start, Pos.new(nil, 0, 4))
|
||||
assert.are.same(range.stop, Pos.new(nil, 0, 10))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('smallest', function()
|
||||
local r1 = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v')
|
||||
local r2 = Range.new(Pos.new(nil, 0, 2), Pos.new(nil, 0, 4), 'v')
|
||||
local r3 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 5), 'v')
|
||||
local smallest = Range.smallest { r1, r2, r3 }
|
||||
assert.are.same(smallest.start, Pos.new(nil, 0, 1))
|
||||
assert.are.same(smallest.stop, Pos.new(nil, 0, 3))
|
||||
end)
|
||||
|
||||
it('clone', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local original = Range.from_lines(nil, 0, 1)
|
||||
local cloned = original:clone()
|
||||
assert.are.same(original.start, cloned.start)
|
||||
assert.are.same(original.stop, cloned.stop)
|
||||
assert.are.same(original.mode, cloned.mode)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('line_count', function()
|
||||
withbuf({ 'line one', 'and line two', 'line three' }, function()
|
||||
local range = Range.from_lines(nil, 0, 2)
|
||||
assert.are.same(range:line_count(), 3)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('to_linewise()', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v')
|
||||
local linewise_range = range:to_linewise()
|
||||
assert.are.same(linewise_range.start.col, 0)
|
||||
assert.are.same(linewise_range.stop.col, Pos.MAX_COL)
|
||||
assert.are.same(linewise_range.mode, 'V')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('is_empty', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 0), nil, 'v')
|
||||
assert.is_true(range:is_empty())
|
||||
|
||||
local range2 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 1), 'v')
|
||||
assert.is_false(range2:is_empty())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('trim_start', function()
|
||||
withbuf({ ' line one', 'line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 9), 'v')
|
||||
local trimmed = range:trim_start()
|
||||
assert.are.same(trimmed.start, Pos.new(nil, 0, 3)) -- should be after the spaces
|
||||
end)
|
||||
end)
|
||||
|
||||
it('trim_stop', function()
|
||||
withbuf({ 'line one ', 'line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 9), 'v')
|
||||
local trimmed = range:trim_stop()
|
||||
assert.are.same(trimmed.stop, Pos.new(nil, 0, 7)) -- should be before the spaces
|
||||
end)
|
||||
end)
|
||||
|
||||
it('contains', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v')
|
||||
local pos = Pos.new(nil, 0, 2)
|
||||
assert.is_true(range:contains(pos))
|
||||
|
||||
pos = Pos.new(nil, 0, 4) -- outside of range
|
||||
assert.is_false(range:contains(pos))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('shrink', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v')
|
||||
local shrunk = range:shrink(1)
|
||||
assert.are.same(shrunk.start, Pos.new(nil, 0, 2))
|
||||
assert.are.same(shrunk.stop, Pos.new(nil, 1, 2))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('must_shrink', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v')
|
||||
local shrunk = range:must_shrink(1)
|
||||
assert.are.same(shrunk.start, Pos.new(nil, 0, 2))
|
||||
assert.are.same(shrunk.stop, Pos.new(nil, 1, 2))
|
||||
|
||||
assert.has.error(function() range:must_shrink(100) end, 'error in Range:must_shrink: Range:shrink() returned nil')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('set_visual_selection', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.from_lines(nil, 0, 1)
|
||||
range:set_visual_selection()
|
||||
|
||||
assert.are.same(Pos.from_pos 'v', Pos.new(nil, 0, 0))
|
||||
assert.are.same(Pos.from_pos '.', Pos.new(nil, 1, 11))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('selections set to past the EOL should not error', function()
|
||||
withbuf({ 'Rg SET NAMES' }, function()
|
||||
local b = vim.api.nvim_get_current_buf()
|
||||
local r = Range.new(Pos.new(b, 0, 3), Pos.new(b, 0, 12), 'v')
|
||||
r:replace 'bleh'
|
||||
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
|
||||
withbuf({ 'Rg SET NAMES' }, function()
|
||||
local b = vim.api.nvim_get_current_buf()
|
||||
local r = Range.new(Pos.new(b, 0, 3), Pos.new(b, 0, 11), 'v')
|
||||
r:replace 'bleh'
|
||||
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace updates Range.stop: same line', function()
|
||||
withbuf({ 'The quick brown fox jumps over the lazy dog' }, function()
|
||||
local b = vim.api.nvim_get_current_buf()
|
||||
local r = Range.new(Pos.new(b, 0, 4), Pos.new(b, 0, 8), 'v')
|
||||
|
||||
r:replace 'bleh1'
|
||||
assert.are.same({ 'The bleh1 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
|
||||
r:replace 'bleh2'
|
||||
assert.are.same({ 'The bleh2 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace updates Range.stop: multi-line', function()
|
||||
withbuf({
|
||||
'The quick brown fox jumps',
|
||||
'over the lazy dog',
|
||||
}, function()
|
||||
local b = vim.api.nvim_get_current_buf()
|
||||
local r = Range.new(Pos.new(b, 0, 20), Pos.new(b, 1, 3), 'v')
|
||||
assert.are.same({ 'jumps', 'over' }, r:lines())
|
||||
|
||||
r:replace 'bleh1'
|
||||
assert.are.same({ 'The quick brown fox bleh1 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
|
||||
r:replace 'blehGoo2'
|
||||
assert.are.same({ 'The quick brown fox blehGoo2 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace updates Range.stop: multi-line (blockwise)', function()
|
||||
withbuf({
|
||||
'The quick brown',
|
||||
'fox',
|
||||
'jumps',
|
||||
'over',
|
||||
'the lazy dog',
|
||||
}, function()
|
||||
local b = vim.api.nvim_get_current_buf()
|
||||
local r = Range.new(Pos.new(b, 1, 0), Pos.new(b, 3, Pos.MAX_COL), 'V')
|
||||
assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
|
||||
|
||||
r:replace { 'bleh1', 'bleh2' }
|
||||
assert.are.same({
|
||||
'The quick brown',
|
||||
'bleh1',
|
||||
'bleh2',
|
||||
'the lazy dog',
|
||||
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
|
||||
r:replace 'blehGoo2'
|
||||
assert.are.same({
|
||||
'The quick brown',
|
||||
'blehGoo2',
|
||||
'the lazy dog',
|
||||
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('replace after delete', function()
|
||||
withbuf({
|
||||
'The quick brown',
|
||||
'fox',
|
||||
'jumps',
|
||||
'over',
|
||||
'the lazy dog',
|
||||
}, function()
|
||||
local b = vim.api.nvim_get_current_buf()
|
||||
local r = Range.new(Pos.new(b, 1, 0), Pos.new(b, 3, Pos.MAX_COL), 'V')
|
||||
assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
|
||||
|
||||
r:replace(nil)
|
||||
assert.are.same({
|
||||
'The quick brown',
|
||||
'the lazy dog',
|
||||
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
|
||||
r:replace { 'blehGoo2', '' }
|
||||
assert.are.same({
|
||||
'The quick brown',
|
||||
'blehGoo2',
|
||||
'the lazy dog',
|
||||
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
1694
spec/u_spec.lua
Normal file
1694
spec/u_spec.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
local function withbuf(lines, f)
|
||||
vim.go.swapfile = false
|
||||
|
||||
vim.cmd.new()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
|
||||
local ok, result = pcall(f)
|
||||
vim.cmd.bdelete { bang = true }
|
||||
if not ok then error(result) end
|
||||
end
|
||||
return withbuf
|
||||
@@ -1,6 +1,10 @@
|
||||
syntax = "LuaJIT"
|
||||
call_parentheses = "None"
|
||||
collapse_simple_statement = "Always"
|
||||
column_width = 120
|
||||
column_width = 100
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
quote_style = "AutoPreferSingle"
|
||||
|
||||
[sort_requires]
|
||||
enabled = true
|
||||
|
||||
16
u-0.0.0-0.rockspec
Normal file
16
u-0.0.0-0.rockspec
Normal file
@@ -0,0 +1,16 @@
|
||||
rockspec_format = '3.0'
|
||||
package = 'u'
|
||||
version = '0.0.0-0'
|
||||
|
||||
source = {
|
||||
url = 'https://github.com/jrop/u.nvim',
|
||||
}
|
||||
|
||||
dependencies = {
|
||||
'lua = 5.1',
|
||||
}
|
||||
|
||||
test_dependencies = {
|
||||
'busted == 2.2.0-1',
|
||||
'luacov == 0.16.0-1',
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package = "u.nvim"
|
||||
version = "0.2.0-1"
|
||||
source = {
|
||||
url = "git+https://github.com/jrop/u.nvim"
|
||||
}
|
||||
description = {
|
||||
summary = "nvim – a powerful Lua library designed to enhance your text manipulation experience in NeoVim, focusing primarily on a context-aware \"Range\" utility.",
|
||||
detailed = "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.",
|
||||
homepage = "https://github.com/jrop/u.nvim",
|
||||
license = "MIT"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["u.buffer"] = "lua/u/buffer.lua",
|
||||
["u.codewriter"] = "lua/u/codewriter.lua",
|
||||
["u.opkeymap"] = "lua/u/opkeymap.lua",
|
||||
["u.pos"] = "lua/u/pos.lua",
|
||||
["u.range"] = "lua/u/range.lua",
|
||||
["u.repeat"] = "lua/u/repeat.lua",
|
||||
["u.state"] = "lua/u/state.lua",
|
||||
["u.utils"] = "lua/u/utils.lua"
|
||||
}
|
||||
}
|
||||
36
vim.yml
36
vim.yml
@@ -1,36 +0,0 @@
|
||||
---
|
||||
base: lua51
|
||||
globals:
|
||||
vim:
|
||||
any: true
|
||||
assert.are.same:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.are_not.same:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.has.error:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.is_true:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.is_false:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
describe:
|
||||
args:
|
||||
- type: string
|
||||
- type: function
|
||||
it:
|
||||
args:
|
||||
- type: string
|
||||
- type: function
|
||||
before_each:
|
||||
args:
|
||||
- type: function
|
||||
Reference in New Issue
Block a user