mirror of
https://github.com/nvim-neotest/nvim-nio.git
synced 2024-09-16 14:24:04 +02:00
feat: initial plugin
This commit is contained in:
parent
b6448cb770
commit
d6f92542f1
34 changed files with 7245 additions and 0 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: rcarriga
|
103
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
103
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: ''
|
||||||
|
assignees: rcarriga
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**NeoVim Version**
|
||||||
|
Output of `nvim --version`
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Please provide a minimal `init.lua` to reproduce which can be run as the following:
|
||||||
|
```sh
|
||||||
|
nvim --clean -u minimal.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
You can edit the following example file to include your adapters and other required setup.
|
||||||
|
```lua
|
||||||
|
-- ignore default config and plugins
|
||||||
|
vim.opt.runtimepath:remove(vim.fn.expand("~/.config/nvim"))
|
||||||
|
vim.opt.packpath:remove(vim.fn.expand("~/.local/share/nvim/site"))
|
||||||
|
vim.opt.termguicolors = true
|
||||||
|
|
||||||
|
-- append test directory
|
||||||
|
local test_dir = "/tmp/nvim-config"
|
||||||
|
vim.opt.runtimepath:append(vim.fn.expand(test_dir))
|
||||||
|
vim.opt.packpath:append(vim.fn.expand(test_dir))
|
||||||
|
|
||||||
|
-- install packer
|
||||||
|
local install_path = test_dir .. "/pack/packer/start/packer.nvim"
|
||||||
|
local install_plugins = false
|
||||||
|
|
||||||
|
if vim.fn.empty(vim.fn.glob(install_path)) > 0 then
|
||||||
|
vim.cmd("!git clone https://github.com/wbthomason/packer.nvim " .. install_path)
|
||||||
|
vim.cmd("packadd packer.nvim")
|
||||||
|
install_plugins = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local packer = require("packer")
|
||||||
|
|
||||||
|
packer.init({
|
||||||
|
package_root = test_dir .. "/pack",
|
||||||
|
compile_path = test_dir .. "/plugin/packer_compiled.lua",
|
||||||
|
})
|
||||||
|
|
||||||
|
packer.startup(function(use)
|
||||||
|
use("wbthomason/packer.nvim")
|
||||||
|
|
||||||
|
use({
|
||||||
|
"nvim-neotest/neotest",
|
||||||
|
requires = {
|
||||||
|
"vim-test/vim-test",
|
||||||
|
"nvim-lua/plenary.nvim",
|
||||||
|
"nvim-treesitter/nvim-treesitter",
|
||||||
|
"antoinemadec/FixCursorHold.nvim",
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
require("neotest").setup({
|
||||||
|
adapters = {},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
if install_plugins then
|
||||||
|
packer.sync()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.cmd([[
|
||||||
|
command! NeotestSummary lua require("neotest").summary.toggle()
|
||||||
|
command! NeotestFile lua require("neotest").run.run(vim.fn.expand("%"))
|
||||||
|
command! Neotest lua require("neotest").run.run(vim.fn.getcwd())
|
||||||
|
command! NeotestNearest lua require("neotest").run.run()
|
||||||
|
command! NeotestDebug lua require("neotest").run.run({ strategy = "dap" })
|
||||||
|
command! NeotestAttach lua require("neotest").run.attach()
|
||||||
|
command! NeotestOutput lua require("neotest").output.open()
|
||||||
|
]])
|
||||||
|
```
|
||||||
|
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
Please provide example test files to reproduce.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
1. Wipe the `neotest.log` file in `stdpath("log")` or `stdpath("data")`.
|
||||||
|
2. Set `log_level = vim.log.levels.DEBUG` in your neotest setup config.
|
||||||
|
3. Reproduce the issue.
|
||||||
|
4. Provide the new logs.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
55
.github/workflows/docgen.yaml
vendored
Normal file
55
.github/workflows/docgen.yaml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# Taken from telescope
|
||||||
|
name: Generate docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- master
|
||||||
|
pull_request: ~
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-sources:
|
||||||
|
name: Generate docs
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-20.04
|
||||||
|
url: https://github.com/neovim/neovim/releases/download/v0.5.1/nvim-linux64.tar.gz
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: date +%F > todays-date
|
||||||
|
- name: Restore cache for today's nightly.
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: _neovim
|
||||||
|
key: ${{ runner.os }}-${{ matrix.url }}-${{ hashFiles('todays-date') }}
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
test -d _neovim || {
|
||||||
|
mkdir -p _neovim
|
||||||
|
curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
|
||||||
|
}
|
||||||
|
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
|
||||||
|
git clone --depth 1 https://github.com/echasnovski/mini.nvim ~/.local/share/nvim/site/pack/vendor/start/mini.nvim
|
||||||
|
- name: Generating docs
|
||||||
|
run: |
|
||||||
|
export PATH="${PWD}/_neovim/bin:${PATH}"
|
||||||
|
export VIM="${PWD}/_neovim/share/nvim/runtime"
|
||||||
|
nvim --version
|
||||||
|
./scripts/docgen
|
||||||
|
- name: Update documentation
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
COMMIT_MSG: |
|
||||||
|
docs: update doc/neotest.txt
|
||||||
|
skip-checks: true
|
||||||
|
run: |
|
||||||
|
git config user.email "actions@github"
|
||||||
|
git config user.name "Github Actions"
|
||||||
|
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
|
||||||
|
git add doc/
|
||||||
|
# Only commit and push if we have changes
|
||||||
|
git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF})
|
16
.github/workflows/issues.yaml
vendored
Normal file
16
.github/workflows/issues.yaml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
name: Add issues to tracking project
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-to-project:
|
||||||
|
name: Add issue to project
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/add-to-project@v0.3.0
|
||||||
|
with:
|
||||||
|
project-url: https://github.com/orgs/nvim-neotest/projects/1
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
29
.github/workflows/luarocks-release.yaml
vendored
Normal file
29
.github/workflows/luarocks-release.yaml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
pull_request: # Tests the luarocks installation without releasing on PR
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
luarocks-upload:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Get Version
|
||||||
|
# tags do not trigger the workflow when they are created by other workflows or releases
|
||||||
|
run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV
|
||||||
|
- name: LuaRocks Upload
|
||||||
|
uses: nvim-neorocks/luarocks-tag-release@v5
|
||||||
|
env:
|
||||||
|
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||||
|
with:
|
||||||
|
version: ${{ env.LUAROCKS_VERSION }}
|
||||||
|
dependencies: |
|
||||||
|
plenary.nvim
|
77
.github/workflows/workflow.yaml
vendored
Normal file
77
.github/workflows/workflow.yaml
vendored
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
name: neotest Workflow
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request: ~
|
||||||
|
jobs:
|
||||||
|
style:
|
||||||
|
name: style
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: JohnnyMorganz/stylua-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: latest
|
||||||
|
args: --check lua/ tests/
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: tests
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
rev: nightly/nvim-linux64.tar.gz
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
rev: v0.9.1/nvim-linux64.tar.gz
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: date +%F > todays-date
|
||||||
|
- name: Restore cache for today's nightly.
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: _neovim
|
||||||
|
key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }}
|
||||||
|
|
||||||
|
- name: Prepare dependencies
|
||||||
|
run: |
|
||||||
|
test -d _neovim || {
|
||||||
|
mkdir -p _neovim
|
||||||
|
curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
|
||||||
|
}
|
||||||
|
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
|
||||||
|
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
|
||||||
|
ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
|
||||||
|
export PATH="${PWD}/_neovim/bin:${PATH}"
|
||||||
|
export VIM="${PWD}/_neovim/share/nvim/runtime"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
export PATH="${PWD}/_neovim/bin:${PATH}"
|
||||||
|
export VIM="${PWD}/_neovim/share/nvim/runtime"
|
||||||
|
nvim --version
|
||||||
|
./scripts/test
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: release
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- style
|
||||||
|
- tests
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: npx semantic-release
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
neovim/
|
||||||
|
plenary.nvim/
|
||||||
|
doc/tags
|
||||||
|
Session.vim
|
12
.releaserc.json
Normal file
12
.releaserc.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"successComment": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
21
LICENCE.md
Normal file
21
LICENCE.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Rónán Carrigan
|
||||||
|
|
||||||
|
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 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.
|
178
README.md
178
README.md
|
@ -0,0 +1,178 @@
|
||||||
|
# nvim-nio
|
||||||
|
|
||||||
|
A library for asynchronous IO in Neovim, inspired by the asyncio library in Python. The library focuses on providing
|
||||||
|
both common asynchronous primitives and asynchronous APIs for Neovim's core.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
Work has been ongoing around async libraries in Neovim for years, with a lot of discussion around a [Neovim core
|
||||||
|
implementation](https://github.com/neovim/neovim/issues/19624). A lot of the motivation behind this library can be seen
|
||||||
|
in that discussion.
|
||||||
|
|
||||||
|
nvim-nio aims to provide a simple interface to Lua coroutines that doesn't feel like it gets in the way of your actual
|
||||||
|
logic. You won't even know you're using them. An example of this is error handling. With other libraries, a custom
|
||||||
|
`pcall` or some other custom handling must be used to catch errors. With nvim-nio, Lua's built-in `pcall` works exactly
|
||||||
|
as you'd expect.
|
||||||
|
|
||||||
|
nvim-nio is focused on providing a great developer experience. The API is well documented with examples and full type
|
||||||
|
annotations, which can all be used by the Lua LSP. It's recommended to use
|
||||||
|
[neodev.nvim](https://github.com/folke/neodev.nvim) to get LSP support.
|
||||||
|
|
||||||
|
![image](https://github.com/nvim-lua/plenary.nvim/assets/24252670/0dda462c-0b5c-4300-8e65-b7218e3d2c1e)
|
||||||
|
|
||||||
|
Credit to the async library in [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) and
|
||||||
|
[async.nvim](https://github.com/lewis6991/async.nvim) for inspiring nvim-nio and its implementation.
|
||||||
|
If Neovim core integrates an async library, nvim-nio will aim to maintain compatibility with it if possible.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install with your favourite package manager
|
||||||
|
|
||||||
|
[lazy.nvim](https://github.com/folke/lazy.nvim)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ "nvim-neotest/nvim-nio" }
|
||||||
|
```
|
||||||
|
|
||||||
|
[dein](https://github.com/Shougo/dein.vim):
|
||||||
|
|
||||||
|
```vim
|
||||||
|
call dein#add("nvim-neotest/nvim-nio")
|
||||||
|
```
|
||||||
|
|
||||||
|
[vim-plug](https://github.com/junegunn/vim-plug)
|
||||||
|
|
||||||
|
```vim
|
||||||
|
Plug 'nvim-neotest/nvim-nio'
|
||||||
|
```
|
||||||
|
|
||||||
|
[packer.nvim](https://github.com/wbthomason/packer.nvim)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
use { "nvim-neotest/nvim-nio" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
There are no configuration options currently available.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
nvim-nio is based on the concept of tasks. These tasks represent a series of asynchronous actions that run in a single
|
||||||
|
context. Under the hood, each task is running on a separate lua coroutine.
|
||||||
|
|
||||||
|
Tasks are created by providing an async function to `nio.run`. All async
|
||||||
|
functions must be called from a task.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local nio = require("nio")
|
||||||
|
|
||||||
|
local task = nio.run(function()
|
||||||
|
nio.sleep(10)
|
||||||
|
print("Hello world")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
For simple use cases tasks won't be too important but they support features such as cancelling and retrieving stack traces.
|
||||||
|
|
||||||
|
nvim-nio comes with built-in modules to help with writing async code. See `:help nvim-nio` for extensive documentation.
|
||||||
|
|
||||||
|
`nio.control`: Primitives for flow control in async functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local event = nio.control.event()
|
||||||
|
|
||||||
|
local worker = nio.tasks.run(function()
|
||||||
|
nio.sleep(1000)
|
||||||
|
event.set()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local listeners = {
|
||||||
|
nio.tasks.run(function()
|
||||||
|
event.wait()
|
||||||
|
print("First listener notified")
|
||||||
|
end),
|
||||||
|
nio.tasks.run(function()
|
||||||
|
event.wait()
|
||||||
|
print("Second listener notified")
|
||||||
|
end),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`nio.lsp`: A fully typed and documented async LSP client library, generated from the LSP specification.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local client = nio.lsp.get_clients({ name = "lua_ls" })[1]
|
||||||
|
|
||||||
|
local err, response = client.request.textDocument_semanticTokens_full({
|
||||||
|
textDocument = { uri = vim.uri_from_bufnr(0) },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert(not err, err)
|
||||||
|
|
||||||
|
for _, token in pairs(response.data) do
|
||||||
|
print(token)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`nio.uv`: Async versions of `vim.loop` functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local file_path = "README.md"
|
||||||
|
|
||||||
|
local open_err, file_fd = nio.uv.fs_open(file_path, "r", 438)
|
||||||
|
assert(not open_err, open_err)
|
||||||
|
|
||||||
|
local stat_err, stat = nio.uv.fs_fstat(file_fd)
|
||||||
|
assert(not stat_err, stat_err)
|
||||||
|
|
||||||
|
local read_err, data = nio.uv.fs_read(file_fd, stat.size, 0)
|
||||||
|
assert(not read_err, read_err)
|
||||||
|
|
||||||
|
local close_err = nio.uv.fs_close(file_fd)
|
||||||
|
assert(not close_err, close_err)
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
`nio.ui`: Async versions of vim.ui functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local value = nio.ui.input({ prompt = "Enter something: " })
|
||||||
|
print(("You entered: %s"):format(value))
|
||||||
|
```
|
||||||
|
|
||||||
|
`nio.tests`: Async versions of plenary.nvim's test functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
nio.tests.it("notifies listeners", function()
|
||||||
|
local event = nio.control.event()
|
||||||
|
local notified = 0
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
event.wait()
|
||||||
|
notified = notified + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
event.set()
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals(10, notified)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
It is also easy to wrap callback style functions to make them asynchronous using `nio.wrap`, which allows easily
|
||||||
|
integrating third-party APIs with nvim-nio.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local nio = require("nio")
|
||||||
|
|
||||||
|
local sleep = nio.wrap(function(ms, cb)
|
||||||
|
vim.defer_fn(cb, ms)
|
||||||
|
end, 2)
|
||||||
|
|
||||||
|
nio.run(function()
|
||||||
|
sleep(10)
|
||||||
|
print("Slept for 10ms")
|
||||||
|
end)
|
||||||
|
```
|
556
doc/nio.txt
Normal file
556
doc/nio.txt
Normal file
|
@ -0,0 +1,556 @@
|
||||||
|
*nvim-nio.txt* A library for asynchronous IO in Neovim
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
nvim-nio *nvim-nio*
|
||||||
|
|
||||||
|
nio....................................................................|nio|
|
||||||
|
nio.control....................................................|nio.control|
|
||||||
|
nio.lsp............................................................|nio.lsp|
|
||||||
|
nio.uv..............................................................|nio.uv|
|
||||||
|
nio.ui..............................................................|nio.ui|
|
||||||
|
nio.tests........................................................|nio.tests|
|
||||||
|
|
||||||
|
|
||||||
|
A library for asynchronous IO in Neovim, inspired by the asyncio library in
|
||||||
|
Python. The library focuses on providing both common asynchronous primitives
|
||||||
|
and asynchronous APIs for Neovim's core.
|
||||||
|
|
||||||
|
nio *nio*
|
||||||
|
|
||||||
|
|
||||||
|
*nio.run()*
|
||||||
|
`run`({func}, {cb})
|
||||||
|
|
||||||
|
Run a function in an async context. This is the entrypoint to all async
|
||||||
|
functionality.
|
||||||
|
>lua
|
||||||
|
local nio = require("nio")
|
||||||
|
nio.run(function()
|
||||||
|
nio.sleep(10)
|
||||||
|
print("Hello world")
|
||||||
|
end)
|
||||||
|
<
|
||||||
|
Parameters~
|
||||||
|
{func} `(function)`
|
||||||
|
{cb?} `(fun(success: boolean,...))` Callback to invoke when the task is
|
||||||
|
complete. If success is false then the parameters will be an error message and
|
||||||
|
a traceback of the error, otherwise it will be the result of the async
|
||||||
|
function.
|
||||||
|
Return~
|
||||||
|
`(nio.tasks.Task)`
|
||||||
|
|
||||||
|
*nio.wrap()*
|
||||||
|
`wrap`({func}, {argc})
|
||||||
|
|
||||||
|
Creates an async function with a callback style function.
|
||||||
|
>lua
|
||||||
|
local nio = require("nio")
|
||||||
|
local sleep = nio.wrap(function(ms, cb)
|
||||||
|
vim.defer_fn(cb, ms)
|
||||||
|
end, 2)
|
||||||
|
|
||||||
|
nio.run(function()
|
||||||
|
sleep(10)
|
||||||
|
print("Slept for 10ms")
|
||||||
|
end)
|
||||||
|
<
|
||||||
|
Parameters~
|
||||||
|
{func} `(function)` A callback style function to be converted. The last
|
||||||
|
argument must be the callback.
|
||||||
|
{argc} `(integer)` The number of arguments of func. Must be included.
|
||||||
|
Return~
|
||||||
|
`(function)` Returns an async function
|
||||||
|
|
||||||
|
*nio.create()*
|
||||||
|
`create`({func}, {argc})
|
||||||
|
|
||||||
|
Takes an async function and returns a function that can run in both async
|
||||||
|
and non async contexts. When running in an async context, the function can
|
||||||
|
return values, but when run in a non-async context, a Task object is
|
||||||
|
returned and an extra callback argument can be supplied to receive the
|
||||||
|
result, with the same signature as the callback for `nio.run`.
|
||||||
|
|
||||||
|
This is useful for APIs where users don't want to create async
|
||||||
|
contexts but which are still used in async contexts internally.
|
||||||
|
Parameters~
|
||||||
|
{func} `(async fun(...))`
|
||||||
|
{argc?} `(integer)` The number of arguments of func. Must be included if there
|
||||||
|
are arguments.
|
||||||
|
|
||||||
|
*nio.gather()*
|
||||||
|
`gather`({functions})
|
||||||
|
|
||||||
|
Run a collection of async functions concurrently and return when
|
||||||
|
all have finished.
|
||||||
|
If any of the functions fail, all pending tasks will be cancelled and the
|
||||||
|
error will be re-raised
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{functions} `(function[])`
|
||||||
|
Return~
|
||||||
|
`(any[])` Results of all functions
|
||||||
|
|
||||||
|
*nio.first()*
|
||||||
|
`first`({functions})
|
||||||
|
|
||||||
|
Run a collection of async functions concurrently and return the result of
|
||||||
|
the first to finish.
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{functions} `(function[])`
|
||||||
|
Return~
|
||||||
|
`(any)`
|
||||||
|
|
||||||
|
*nio.sleep()*
|
||||||
|
`sleep`({ms})
|
||||||
|
|
||||||
|
Suspend the current task for given time.
|
||||||
|
Parameters~
|
||||||
|
{ms} `(number)` Time in milliseconds
|
||||||
|
|
||||||
|
*nio.scheduler()*
|
||||||
|
`scheduler`()
|
||||||
|
|
||||||
|
Yields to the Neovim scheduler to be able to call the API.
|
||||||
|
|
||||||
|
|
||||||
|
nio.api *nio.api*
|
||||||
|
|
||||||
|
Safely proxies calls to the vim.api module while in an async context.
|
||||||
|
|
||||||
|
nio.fn *nio.fn*
|
||||||
|
|
||||||
|
Safely proxies calls to the vim.fn module while in an async context.
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
nio.control *nio.control*
|
||||||
|
|
||||||
|
|
||||||
|
Provides primitives for flow control in async functions
|
||||||
|
|
||||||
|
*nio.control.event()*
|
||||||
|
`event`()
|
||||||
|
|
||||||
|
Create a new event
|
||||||
|
|
||||||
|
An event can signal to multiple listeners to resume execution
|
||||||
|
The event can be set from a non-async context.
|
||||||
|
|
||||||
|
>lua
|
||||||
|
local event = nio.control.event()
|
||||||
|
|
||||||
|
local worker = nio.tasks.run(function()
|
||||||
|
nio.sleep(1000)
|
||||||
|
event.set()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local listeners = {
|
||||||
|
nio.tasks.run(function()
|
||||||
|
event.wait()
|
||||||
|
print("First listener notified")
|
||||||
|
end),
|
||||||
|
nio.tasks.run(function()
|
||||||
|
event.wait()
|
||||||
|
print("Second listener notified")
|
||||||
|
end),
|
||||||
|
}
|
||||||
|
<
|
||||||
|
Return~
|
||||||
|
`(nio.control.Event)`
|
||||||
|
|
||||||
|
*nio.control.Event*
|
||||||
|
Fields~
|
||||||
|
{set} `(fun(max_woken?: integer): nil)` Set the event and signal to all (or
|
||||||
|
limited number of) listeners that the event has occurred. If max_woken is
|
||||||
|
provided and there are more listeners then the event is cleared immediately
|
||||||
|
{wait} `(async fun(): nil)` Wait for the event to occur, returning immediately
|
||||||
|
if
|
||||||
|
already set
|
||||||
|
{clear} `(fun(): nil)` Clear the event
|
||||||
|
{is_set} `(fun(): boolean)` Returns true if the event is set
|
||||||
|
|
||||||
|
*nio.control.future()*
|
||||||
|
`future`()
|
||||||
|
|
||||||
|
Create a new future
|
||||||
|
|
||||||
|
An future represents a value that will be available at some point and can be awaited upon.
|
||||||
|
The future result can be set from a non-async context.
|
||||||
|
>lua
|
||||||
|
local future = nio.control.future()
|
||||||
|
|
||||||
|
nio.run(function()
|
||||||
|
nio.sleep(100 * math.random(1, 10))
|
||||||
|
if not future.is_set() then
|
||||||
|
future.set("Success!")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
nio.run(function()
|
||||||
|
nio.sleep(100 * math.random(1, 10))
|
||||||
|
if not future.is_set() then
|
||||||
|
future.set_error("Failed!")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local success, value = pcall(future.wait)
|
||||||
|
print(("%s: %s"):format(success, value))
|
||||||
|
<
|
||||||
|
Return~
|
||||||
|
`(nio.control.Future)`
|
||||||
|
|
||||||
|
*nio.control.Future*
|
||||||
|
Fields~
|
||||||
|
{set} `(fun(value): nil)` Set the future value and wake all waiters.
|
||||||
|
{set_error} `(fun(message): nil)` Set the error for this future to raise to
|
||||||
|
waiters
|
||||||
|
{wait} `(async fun(): any)` Wait for the value to be set, returning
|
||||||
|
immediately if already set
|
||||||
|
{is_set} `(fun(): boolean)` Returns true if the future is set
|
||||||
|
|
||||||
|
*nio.control.queue()*
|
||||||
|
`queue`({max_size})
|
||||||
|
|
||||||
|
Create a new FIFO queue with async support.
|
||||||
|
>lua
|
||||||
|
local queue = nio.control.queue()
|
||||||
|
|
||||||
|
local producer = nio.tasks.run(function()
|
||||||
|
for i = 1, 10 do
|
||||||
|
nio.sleep(100)
|
||||||
|
queue.put(i)
|
||||||
|
end
|
||||||
|
queue.put(nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local value = queue.get()
|
||||||
|
if value == nil then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
print(value)
|
||||||
|
end
|
||||||
|
print("Done")
|
||||||
|
<
|
||||||
|
Parameters~
|
||||||
|
{max_size?} `(integer)` The maximum number of items in the queue, defaults to
|
||||||
|
no limit
|
||||||
|
Return~
|
||||||
|
`(nio.control.Queue)`
|
||||||
|
|
||||||
|
*nio.control.Queue*
|
||||||
|
Fields~
|
||||||
|
{size} `(fun(): number)` Returns the number of items in the queue
|
||||||
|
{max_size} `(fun(): number|nil)` Returns the maximum number of items in the
|
||||||
|
queue
|
||||||
|
{get} `(async fun(): any)` Get a value from the queue, blocking if the queue
|
||||||
|
is empty
|
||||||
|
{get_nowait} `(fun(): any)` Get a value from the queue, erroring if queue is
|
||||||
|
empty.
|
||||||
|
{put} `(async fun(value: any): nil)` Put a value into the queue
|
||||||
|
{put_nowait} `(fun(value: any): nil)` Put a value into the queue, erroring if
|
||||||
|
queue is full.
|
||||||
|
|
||||||
|
*nio.control.semaphore()*
|
||||||
|
`semaphore`({value})
|
||||||
|
|
||||||
|
Create an async semaphore that allows up to a given number of acquisitions.
|
||||||
|
|
||||||
|
>lua
|
||||||
|
local semaphore = nio.control.semaphore(2)
|
||||||
|
|
||||||
|
local value = 0
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
semaphore.with(function()
|
||||||
|
value = value + 1
|
||||||
|
|
||||||
|
nio.sleep(10)
|
||||||
|
print(value) -- Never more than 2
|
||||||
|
|
||||||
|
value = value - 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
<
|
||||||
|
Parameters~
|
||||||
|
{value} `(integer)` The number of allowed concurrent acquisitions
|
||||||
|
Return~
|
||||||
|
`(nio.control.Semaphore)`
|
||||||
|
|
||||||
|
*nio.control.Semaphore*
|
||||||
|
Fields~
|
||||||
|
{with} `(async fun(callback: fun(): nil): nil)` Run the callback with the
|
||||||
|
semaphore acquired
|
||||||
|
{acquire} `(async fun(): nil)` Acquire the semaphore
|
||||||
|
{release} `(fun(): nil)` Release the semaphore
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
nio.lsp *nio.lsp*
|
||||||
|
|
||||||
|
|
||||||
|
*nio.lsp.Client*
|
||||||
|
Fields~
|
||||||
|
{request} `(nio.lsp.RequestClient)` Interface to all requests that can be sent
|
||||||
|
by the client
|
||||||
|
{notify} `(nio.lsp.NotifyClient)` Interface to all notifications that can be
|
||||||
|
sent by the client
|
||||||
|
{server_capabilities} `(nio.lsp.types.ServerCapabilities)`
|
||||||
|
|
||||||
|
*nio.lsp.get_clients()*
|
||||||
|
`get_clients`({filters})
|
||||||
|
|
||||||
|
Get active clients, optionally matching the given filters
|
||||||
|
Equivalent to |vim.lsp.get_clients|
|
||||||
|
Parameters~
|
||||||
|
{filters?} `(nio.lsp.GetClientsFilters)`
|
||||||
|
Return~
|
||||||
|
`(nio.lsp.Client[])`
|
||||||
|
|
||||||
|
*nio.lsp.GetClientsFilters*
|
||||||
|
Fields~
|
||||||
|
{id?} `(integer)`
|
||||||
|
{name?} `(string)`
|
||||||
|
{bufnr?} `(integer)`
|
||||||
|
{method?} `(string)`
|
||||||
|
|
||||||
|
*nio.lsp.client()*
|
||||||
|
`client`({client_id})
|
||||||
|
|
||||||
|
an async client for the given client id
|
||||||
|
Parameters~
|
||||||
|
{client_id} `(integer)`
|
||||||
|
Return~
|
||||||
|
`(nio.lsp.Client)`
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
nio.uv *nio.uv*
|
||||||
|
|
||||||
|
|
||||||
|
Provides asynchronous versions of vim.loop functions.
|
||||||
|
See corresponding function documentation for parameter and return
|
||||||
|
information.
|
||||||
|
>lua
|
||||||
|
local file_path = "README.md"
|
||||||
|
|
||||||
|
local open_err, file_fd = nio.uv.fs_open(file_path, "r", 438)
|
||||||
|
assert(not open_err, open_err)
|
||||||
|
|
||||||
|
local stat_err, stat = nio.uv.fs_fstat(file_fd)
|
||||||
|
assert(not stat_err, stat_err)
|
||||||
|
|
||||||
|
local read_err, data = nio.uv.fs_read(file_fd, stat.size, 0)
|
||||||
|
assert(not read_err, read_err)
|
||||||
|
|
||||||
|
local close_err = nio.uv.fs_close(file_fd)
|
||||||
|
assert(not close_err, close_err)
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
<
|
||||||
|
|
||||||
|
Fields~
|
||||||
|
{close} `(async fun(handle: nio.uv.Handle))`
|
||||||
|
{fs_open} `(async fun(path: any, flags: any, mode: any):
|
||||||
|
(string|nil,integer|nil))`
|
||||||
|
{fs_read} `(async fun(fd: integer, size: integer, offset?: integer):
|
||||||
|
(string|nil,string|nil))`
|
||||||
|
{fs_close} `(async fun(fd: integer): (string|nil,boolean|nil))`
|
||||||
|
{fs_unlink} `(async fun(path: string): (string|nil,boolean|nil))`
|
||||||
|
{fs_write} `(async fun(fd: any, data: any, offset?: any):
|
||||||
|
(string|nil,integer|nil))`
|
||||||
|
{fs_mkdir} `(async fun(path: string, mode: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_mkdtemp} `(async fun(template: string): (string|nil,string|nil))`
|
||||||
|
{fs_rmdir} `(async fun(path: string): (string|nil,boolean|nil))`
|
||||||
|
{fs_stat} `(async fun(path: string): (string|nil,nio.uv.Stat|nil))`
|
||||||
|
{fs_fstat} `(async fun(fd: integer): (string|nil,nio.uv.Stat|nil))`
|
||||||
|
{fs_lstat} `(async fun(path: string): (string|nil,nio.uv.Stat|nil))`
|
||||||
|
{fs_statfs} `(async fun(path: string): (string|nil,nio.uv.StatFs|nil))`
|
||||||
|
{fs_rename} `(async fun(old_path: string, new_path: string):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_fsync} `(async fun(fd: integer): (string|nil,boolean|nil))`
|
||||||
|
{fs_fdatasync} `(async fun(fd: integer): (string|nil,boolean|nil))`
|
||||||
|
{fs_ftruncate} `(async fun(fd: integer, offset: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_sendfile} `(async fun(out_fd: integer, in_fd: integer, in_offset: integer,
|
||||||
|
length: integer): (string|nil,integer|nil))`
|
||||||
|
{fs_access} `(async fun(path: string, mode: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_chmod} `(async fun(path: string, mode: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_fchmod} `(async fun(fd: integer, mode: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_utime} `(async fun(path: string, atime: number, mtime: number):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_futime} `(async fun(fd: integer, atime: number, mtime: number):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_link} `(async fun(path: string, new_path: string):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_symlink} `(async fun(path: string, new_path: string, flags?: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_readlink} `(async fun(path: string): (string|nil,string|nil))`
|
||||||
|
{fs_realpath} `(async fun(path: string): (string|nil,string|nil))`
|
||||||
|
{fs_chown} `(async fun(path: string, uid: integer, gid: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_fchown} `(async fun(fd: integer, uid: integer, gid: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_lchown} `(async fun(path: string, uid: integer, gid: integer):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_copyfile} `(async fun(path: any, new_path: any, flags?: any):
|
||||||
|
(string|nil,boolean|nil))`
|
||||||
|
{fs_opendir} `(async fun(path: string, entries?: integer):
|
||||||
|
(string|nil,nio.uv.Dir|nil))`
|
||||||
|
{fs_readdir} `(async fun(dir: nio.uv.Dir):
|
||||||
|
(string|nil,nio.uv.DirEntry[]|nil))`
|
||||||
|
{fs_closedir} `(async fun(dir: nio.uv.Dir): (string|nil,boolean|nil))`
|
||||||
|
{fs_scandir} `(async fun(path: string): (string|nil,nio.uv.DirEntry[]|nil))`
|
||||||
|
{shutdown} `(async fun(stream: nio.uv.Stream): string|nil)`
|
||||||
|
{listen} `(async fun(stream: nio.uv.Stream, backlog: integer): string|nil)`
|
||||||
|
{write} `(async fun(stream: nio.uv.Stream, data: string|string[]):
|
||||||
|
string|nil)`
|
||||||
|
{write2} `(async fun(stream: nio.uv.Stream, data: string|string[],
|
||||||
|
send_handle: nio.uv.Stream): string|nil)`
|
||||||
|
|
||||||
|
*nio.uv.Handle*
|
||||||
|
|
||||||
|
*nio.uv.Stream*
|
||||||
|
Inherits: `nio.uv.Handle`
|
||||||
|
|
||||||
|
|
||||||
|
*nio.uv.Stat*
|
||||||
|
Fields~
|
||||||
|
{dev} `(integer)`
|
||||||
|
{mode} `(integer)`
|
||||||
|
{nlink} `(integer)`
|
||||||
|
{uid} `(integer)`
|
||||||
|
{gid} `(integer)`
|
||||||
|
{rdev} `(integer)`
|
||||||
|
{ino} `(integer)`
|
||||||
|
{size} `(integer)`
|
||||||
|
{blksize} `(integer)`
|
||||||
|
{blocks} `(integer)`
|
||||||
|
{flags} `(integer)`
|
||||||
|
{gen} `(integer)`
|
||||||
|
{atime} `(nio.uv.StatTime)`
|
||||||
|
{mtime} `(nio.uv.StatTime)`
|
||||||
|
{ctime} `(nio.uv.StatTime)`
|
||||||
|
{birthtime} `(nio.uv.StatTime)`
|
||||||
|
{type} `(string)`
|
||||||
|
|
||||||
|
*nio.uv.StatTime*
|
||||||
|
Fields~
|
||||||
|
{sec} `(integer)`
|
||||||
|
{nsec} `(integer)`
|
||||||
|
|
||||||
|
*nio.uv.StatFs*
|
||||||
|
Fields~
|
||||||
|
{type} `(integer)`
|
||||||
|
{bsize} `(integer)`
|
||||||
|
{blocks} `(integer)`
|
||||||
|
{bfree} `(integer)`
|
||||||
|
{bavail} `(integer)`
|
||||||
|
{files} `(integer)`
|
||||||
|
{ffree} `(integer)`
|
||||||
|
|
||||||
|
*nio.uv.Dir*
|
||||||
|
|
||||||
|
*nio.uv.DirEntry*
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
nio.ui *nio.ui*
|
||||||
|
|
||||||
|
|
||||||
|
Async versions of vim.ui functions.
|
||||||
|
|
||||||
|
*nio.ui.input()*
|
||||||
|
`input`({args})
|
||||||
|
|
||||||
|
Prompt the user for input.
|
||||||
|
See |vim.ui.input()| for details.
|
||||||
|
>lua
|
||||||
|
local value = nio.ui.input({ prompt = "Enter something: " })
|
||||||
|
print(("You entered: %s"):format(value))
|
||||||
|
<
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{args} `(nio.ui.InputArgs)`
|
||||||
|
|
||||||
|
*nio.ui.InputArgs*
|
||||||
|
Fields~
|
||||||
|
{prompt} `(string|nil)` Text of the prompt
|
||||||
|
{default} `(string|nil)` Default reply to the input
|
||||||
|
{completion} `(string|nil)` Specifies type of completion supported for input.
|
||||||
|
Supported types are the same that can be supplied to a user-defined command
|
||||||
|
using the "-complete=" argument. See |:command-completion|
|
||||||
|
{highlight} `(function)` Function that will be used for highlighting user
|
||||||
|
inputs.
|
||||||
|
|
||||||
|
*nio.ui.select()*
|
||||||
|
`select`({items}, {args})
|
||||||
|
|
||||||
|
Prompts the user to pick from a list of items
|
||||||
|
See |vim.ui.select()| for details.
|
||||||
|
<
|
||||||
|
local value = nio.ui.select({ "foo", "bar", "baz" }, { prompt = "Select something: " })
|
||||||
|
print(("You entered: %s"):format(value))
|
||||||
|
<
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{items} `(any[])`
|
||||||
|
{args} `(nio.ui.SelectArgs)`
|
||||||
|
|
||||||
|
*nio.ui.SelectArgs*
|
||||||
|
Fields~
|
||||||
|
{prompt} `(string|nil)` Text of the prompt. Defaults to `Select one of:`
|
||||||
|
{format_item} `(function|nil)` Function to format an individual item from
|
||||||
|
`items`. Defaults to `tostring`.
|
||||||
|
{kind} `(string|nil)` Arbitrary hint string indicating the item shape. Plugins
|
||||||
|
reimplementing `vim.ui.select` may wish to use this to infer the structure or
|
||||||
|
semantics of `items`, or the context in which select() was called.
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
nio.tests *nio.tests*
|
||||||
|
|
||||||
|
|
||||||
|
Wrappers around plenary.nvim's test functions for writing async tests
|
||||||
|
>lua
|
||||||
|
a.it("notifies listeners", function()
|
||||||
|
local event = nio.control.event()
|
||||||
|
local notified = 0
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
event.wait()
|
||||||
|
notified = notified + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
event.set()
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals(10, notified)
|
||||||
|
end)
|
||||||
|
|
||||||
|
*nio.tests.it()*
|
||||||
|
`it`({name}, {async_func})
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{name} `(string)`
|
||||||
|
{async_func} `(function)`
|
||||||
|
|
||||||
|
*nio.tests.before_each()*
|
||||||
|
`before_each`({async_func})
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{async_func} `(function)`
|
||||||
|
|
||||||
|
*nio.tests.after_each()*
|
||||||
|
`after_each`({async_func})
|
||||||
|
|
||||||
|
Parameters~
|
||||||
|
{async_func} `(function)`
|
||||||
|
|
||||||
|
|
||||||
|
vim:tw=78:ts=8:noet:ft=help:norl:
|
304
lua/nio/control.lua
Normal file
304
lua/nio/control.lua
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
|
||||||
|
local nio = {}
|
||||||
|
|
||||||
|
---@toc_entry nio.control
|
||||||
|
---@text
|
||||||
|
--- Provides primitives for flow control in async functions
|
||||||
|
---@class nio.control
|
||||||
|
nio.control = {}
|
||||||
|
|
||||||
|
--- Create a new event
|
||||||
|
---
|
||||||
|
--- An event can signal to multiple listeners to resume execution
|
||||||
|
--- The event can be set from a non-async context.
|
||||||
|
---
|
||||||
|
--- ```lua
|
||||||
|
--- local event = nio.control.event()
|
||||||
|
---
|
||||||
|
--- local worker = nio.tasks.run(function()
|
||||||
|
--- nio.sleep(1000)
|
||||||
|
--- event.set()
|
||||||
|
--- end)
|
||||||
|
---
|
||||||
|
--- local listeners = {
|
||||||
|
--- nio.tasks.run(function()
|
||||||
|
--- event.wait()
|
||||||
|
--- print("First listener notified")
|
||||||
|
--- end),
|
||||||
|
--- nio.tasks.run(function()
|
||||||
|
--- event.wait()
|
||||||
|
--- print("Second listener notified")
|
||||||
|
--- end),
|
||||||
|
--- }
|
||||||
|
--- ```
|
||||||
|
---@return nio.control.Event
|
||||||
|
function nio.control.event()
|
||||||
|
local waiters = {}
|
||||||
|
local is_set = false
|
||||||
|
return {
|
||||||
|
is_set = function()
|
||||||
|
return is_set
|
||||||
|
end,
|
||||||
|
set = function(max_woken)
|
||||||
|
if is_set then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
is_set = true
|
||||||
|
local waiters_to_notify = {}
|
||||||
|
max_woken = max_woken or #waiters
|
||||||
|
while #waiters > 0 and #waiters_to_notify < max_woken do
|
||||||
|
waiters_to_notify[#waiters_to_notify + 1] = table.remove(waiters)
|
||||||
|
end
|
||||||
|
if #waiters > 0 then
|
||||||
|
is_set = false
|
||||||
|
end
|
||||||
|
for _, waiter in ipairs(waiters_to_notify) do
|
||||||
|
waiter()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
wait = tasks.wrap(function(callback)
|
||||||
|
if is_set then
|
||||||
|
callback()
|
||||||
|
else
|
||||||
|
waiters[#waiters + 1] = callback
|
||||||
|
end
|
||||||
|
end, 1),
|
||||||
|
clear = function()
|
||||||
|
is_set = false
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class nio.control.Event
|
||||||
|
---@field set fun(max_woken?: integer): nil Set the event and signal to all (or limited number of) listeners that the event has occurred. If max_woken is provided and there are more listeners then the event is cleared immediately
|
||||||
|
---@field wait async fun(): nil Wait for the event to occur, returning immediately if
|
||||||
|
--- already set
|
||||||
|
---@field clear fun(): nil Clear the event
|
||||||
|
---@field is_set fun(): boolean Returns true if the event is set
|
||||||
|
|
||||||
|
--- Create a new future
|
||||||
|
---
|
||||||
|
--- An future represents a value that will be available at some point and can be awaited upon.
|
||||||
|
--- The future result can be set from a non-async context.
|
||||||
|
--- ```lua
|
||||||
|
--- local future = nio.control.future()
|
||||||
|
---
|
||||||
|
--- nio.run(function()
|
||||||
|
--- nio.sleep(100 * math.random(1, 10))
|
||||||
|
--- if not future.is_set() then
|
||||||
|
--- future.set("Success!")
|
||||||
|
--- end
|
||||||
|
--- end)
|
||||||
|
--- nio.run(function()
|
||||||
|
--- nio.sleep(100 * math.random(1, 10))
|
||||||
|
--- if not future.is_set() then
|
||||||
|
--- future.set_error("Failed!")
|
||||||
|
--- end
|
||||||
|
--- end)
|
||||||
|
---
|
||||||
|
--- local success, value = pcall(future.wait)
|
||||||
|
--- print(("%s: %s"):format(success, value))
|
||||||
|
--- ```
|
||||||
|
---@return nio.control.Future
|
||||||
|
function nio.control.future()
|
||||||
|
local waiters = {}
|
||||||
|
local result, err, is_set
|
||||||
|
local wait = tasks.wrap(function(callback)
|
||||||
|
if is_set then
|
||||||
|
callback()
|
||||||
|
else
|
||||||
|
waiters[#waiters + 1] = callback
|
||||||
|
end
|
||||||
|
end, 1)
|
||||||
|
local wake = function()
|
||||||
|
for _, waiter in ipairs(waiters) do
|
||||||
|
waiter()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
is_set = function()
|
||||||
|
return is_set
|
||||||
|
end,
|
||||||
|
set = function(value)
|
||||||
|
if is_set then
|
||||||
|
error("Future already set")
|
||||||
|
end
|
||||||
|
result = value
|
||||||
|
is_set = true
|
||||||
|
wake()
|
||||||
|
end,
|
||||||
|
set_error = function(message)
|
||||||
|
if is_set then
|
||||||
|
error("Future already set")
|
||||||
|
end
|
||||||
|
err = message
|
||||||
|
is_set = true
|
||||||
|
wake()
|
||||||
|
end,
|
||||||
|
wait = function()
|
||||||
|
if not is_set then
|
||||||
|
wait()
|
||||||
|
end
|
||||||
|
|
||||||
|
if err then
|
||||||
|
error(err)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class nio.control.Future
|
||||||
|
---@field set fun(value): nil Set the future value and wake all waiters.
|
||||||
|
---@field set_error fun(message): nil Set the error for this future to raise to
|
||||||
|
---the waiters
|
||||||
|
---@field wait async fun(): any Wait for the value to be set, returning immediately if already set
|
||||||
|
---@field is_set fun(): boolean Returns true if the future is set
|
||||||
|
|
||||||
|
--- Create a new FIFO queue with async support.
|
||||||
|
--- ```lua
|
||||||
|
--- local queue = nio.control.queue()
|
||||||
|
---
|
||||||
|
--- local producer = nio.tasks.run(function()
|
||||||
|
--- for i = 1, 10 do
|
||||||
|
--- nio.sleep(100)
|
||||||
|
--- queue.put(i)
|
||||||
|
--- end
|
||||||
|
--- queue.put(nil)
|
||||||
|
--- end)
|
||||||
|
---
|
||||||
|
--- while true do
|
||||||
|
--- local value = queue.get()
|
||||||
|
--- if value == nil then
|
||||||
|
--- break
|
||||||
|
--- end
|
||||||
|
--- print(value)
|
||||||
|
--- end
|
||||||
|
--- print("Done")
|
||||||
|
--- ```
|
||||||
|
---@param max_size? integer The maximum number of items in the queue, defaults to no limit
|
||||||
|
---@return nio.control.Queue
|
||||||
|
function nio.control.queue(max_size)
|
||||||
|
local items = {}
|
||||||
|
local left_i = 0
|
||||||
|
local right_i = 0
|
||||||
|
local non_empty = nio.control.event()
|
||||||
|
local non_full = nio.control.event()
|
||||||
|
non_full.set()
|
||||||
|
|
||||||
|
local queue = {}
|
||||||
|
|
||||||
|
function queue.size()
|
||||||
|
return right_i - left_i
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue.max_size()
|
||||||
|
return max_size
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue.put(value)
|
||||||
|
non_full.wait()
|
||||||
|
queue.put_nowait(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue.get()
|
||||||
|
non_empty.wait()
|
||||||
|
return queue.get_nowait()
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue.get_nowait()
|
||||||
|
if queue.size() == 0 then
|
||||||
|
error("Queue is empty")
|
||||||
|
end
|
||||||
|
left_i = left_i + 1
|
||||||
|
local item = items[left_i]
|
||||||
|
items[left_i] = nil
|
||||||
|
if left_i == right_i then
|
||||||
|
non_empty.clear()
|
||||||
|
end
|
||||||
|
non_full.set(1)
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue.put_nowait(value)
|
||||||
|
if queue.size() == max_size then
|
||||||
|
error("Queue is full")
|
||||||
|
end
|
||||||
|
right_i = right_i + 1
|
||||||
|
items[right_i] = value
|
||||||
|
non_empty.set(1)
|
||||||
|
if queue.size() == max_size then
|
||||||
|
non_full.clear()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return queue
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class nio.control.Queue
|
||||||
|
---@field size fun(): number Returns the number of items in the queue
|
||||||
|
---@field max_size fun(): number|nil Returns the maximum number of items in the queue
|
||||||
|
---@field get async fun(): any Get a value from the queue, blocking if the queue is empty
|
||||||
|
---@field get_nowait fun(): any Get a value from the queue, erroring if queue is empty.
|
||||||
|
---@field put async fun(value: any): nil Put a value into the queue
|
||||||
|
---@field put_nowait fun(value: any): nil Put a value into the queue, erroring if queue is full.
|
||||||
|
|
||||||
|
--- Create an async semaphore that allows up to a given number of acquisitions.
|
||||||
|
---
|
||||||
|
--- ```lua
|
||||||
|
--- local semaphore = nio.control.semaphore(2)
|
||||||
|
---
|
||||||
|
--- local value = 0
|
||||||
|
--- for _ = 1, 10 do
|
||||||
|
--- nio.run(function()
|
||||||
|
--- semaphore.with(function()
|
||||||
|
--- value = value + 1
|
||||||
|
---
|
||||||
|
--- nio.sleep(10)
|
||||||
|
--- print(value) -- Never more than 2
|
||||||
|
---
|
||||||
|
--- value = value - 1
|
||||||
|
--- end)
|
||||||
|
--- end)
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
---@param value integer The number of allowed concurrent acquisitions
|
||||||
|
---@return nio.control.Semaphore
|
||||||
|
function nio.control.semaphore(value)
|
||||||
|
value = value or 1
|
||||||
|
local released_event = nio.control.event()
|
||||||
|
released_event.set()
|
||||||
|
local acquire = function()
|
||||||
|
released_event.wait()
|
||||||
|
value = value - 1
|
||||||
|
assert(value >= 0, "Semaphore value is negative")
|
||||||
|
if value == 0 then
|
||||||
|
released_event.clear()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local release = function()
|
||||||
|
value = value + 1
|
||||||
|
released_event.set(1)
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
acquire = acquire,
|
||||||
|
release = release,
|
||||||
|
|
||||||
|
with = function(cb)
|
||||||
|
acquire()
|
||||||
|
local success, err = pcall(cb)
|
||||||
|
release()
|
||||||
|
if not success then
|
||||||
|
error(err)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class nio.control.Semaphore
|
||||||
|
---@field with async fun(callback: fun(): nil): nil Run the callback with the semaphore acquired
|
||||||
|
---@field acquire async fun(): nil Acquire the semaphore
|
||||||
|
---@field release fun(): nil Release the semaphore
|
||||||
|
|
||||||
|
return nio.control
|
197
lua/nio/init.lua
Normal file
197
lua/nio/init.lua
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
local nio = {}
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
local control = require("nio.control")
|
||||||
|
local uv = require("nio.uv")
|
||||||
|
local tests = require("nio.tests")
|
||||||
|
local ui = require("nio.ui")
|
||||||
|
local lsp = require("nio.lsp")
|
||||||
|
|
||||||
|
---@tag nvim-nio
|
||||||
|
|
||||||
|
---@toc
|
||||||
|
|
||||||
|
---@text
|
||||||
|
---
|
||||||
|
--- A library for asynchronous IO in Neovim, inspired by the asyncio library in
|
||||||
|
--- Python. The library focuses on providing both common asynchronous primitives
|
||||||
|
--- and asynchronous APIs for Neovim's core.
|
||||||
|
|
||||||
|
---@toc_entry nio
|
||||||
|
---@class nio
|
||||||
|
nio = {}
|
||||||
|
|
||||||
|
nio.control = control
|
||||||
|
nio.uv = uv
|
||||||
|
nio.ui = ui
|
||||||
|
nio.tests = tests
|
||||||
|
nio.tasks = tasks
|
||||||
|
nio.lsp = lsp
|
||||||
|
|
||||||
|
--- Run a function in an async context. This is the entrypoint to all async
|
||||||
|
--- functionality.
|
||||||
|
--- ```lua
|
||||||
|
--- local nio = require("nio")
|
||||||
|
--- nio.run(function()
|
||||||
|
--- nio.sleep(10)
|
||||||
|
--- print("Hello world")
|
||||||
|
--- end)
|
||||||
|
--- ```
|
||||||
|
---@param func function
|
||||||
|
---@param cb? fun(success: boolean,...) Callback to invoke when the task is complete. If success is false then the parameters will be an error message and a traceback of the error, otherwise it will be the result of the async function.
|
||||||
|
---@return nio.tasks.Task
|
||||||
|
function nio.run(func, cb)
|
||||||
|
return tasks.run(func, cb)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates an async function with a callback style function.
|
||||||
|
--- ```lua
|
||||||
|
--- local nio = require("nio")
|
||||||
|
--- local sleep = nio.wrap(function(ms, cb)
|
||||||
|
--- vim.defer_fn(cb, ms)
|
||||||
|
--- end, 2)
|
||||||
|
---
|
||||||
|
--- nio.run(function()
|
||||||
|
--- sleep(10)
|
||||||
|
--- print("Slept for 10ms")
|
||||||
|
--- end)
|
||||||
|
--- ```
|
||||||
|
---@param func function A callback style function to be converted. The last argument must be the callback.
|
||||||
|
---@param argc integer The number of arguments of func. Must be included.
|
||||||
|
---@return function Returns an async function
|
||||||
|
function nio.wrap(func, argc)
|
||||||
|
return tasks.wrap(func, argc)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Takes an async function and returns a function that can run in both async
|
||||||
|
--- and non async contexts. When running in an async context, the function can
|
||||||
|
--- return values, but when run in a non-async context, a Task object is
|
||||||
|
--- returned and an extra callback argument can be supplied to receive the
|
||||||
|
--- result, with the same signature as the callback for `nio.run`.
|
||||||
|
---
|
||||||
|
--- This is useful for APIs where users don't want to create async
|
||||||
|
--- contexts but which are still used in async contexts internally.
|
||||||
|
---@param func async fun(...)
|
||||||
|
---@param argc? integer The number of arguments of func. Must be included if there are arguments.
|
||||||
|
function nio.create(func, argc)
|
||||||
|
return tasks.create(func, argc)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Run a collection of async functions concurrently and return when
|
||||||
|
--- all have finished.
|
||||||
|
--- If any of the functions fail, all pending tasks will be cancelled and the
|
||||||
|
--- error will be re-raised
|
||||||
|
---@async
|
||||||
|
---@param functions function[]
|
||||||
|
---@return any[] Results of all functions
|
||||||
|
function nio.gather(functions)
|
||||||
|
local results = {}
|
||||||
|
|
||||||
|
local done_event = control.event()
|
||||||
|
|
||||||
|
local err
|
||||||
|
local running = {}
|
||||||
|
for i, func in ipairs(functions) do
|
||||||
|
local task = tasks.run(func, function(success, ...)
|
||||||
|
if not success then
|
||||||
|
err = ...
|
||||||
|
done_event.set()
|
||||||
|
end
|
||||||
|
results[#results + 1] = { i = i, success = success, result = ... }
|
||||||
|
if #results == #functions then
|
||||||
|
done_event.set()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
running[#running + 1] = task
|
||||||
|
end
|
||||||
|
done_event.wait()
|
||||||
|
if err then
|
||||||
|
for _, task in ipairs(running) do
|
||||||
|
task.cancel()
|
||||||
|
end
|
||||||
|
error(err)
|
||||||
|
end
|
||||||
|
local sorted = {}
|
||||||
|
for _, result in ipairs(results) do
|
||||||
|
sorted[result.i] = result.result
|
||||||
|
end
|
||||||
|
return sorted
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Run a collection of async functions concurrently and return the result of
|
||||||
|
--- the first to finish.
|
||||||
|
---@async
|
||||||
|
---@param functions function[]
|
||||||
|
---@return any
|
||||||
|
function nio.first(functions)
|
||||||
|
local running_tasks = {}
|
||||||
|
local event = control.event()
|
||||||
|
local failed, result
|
||||||
|
|
||||||
|
for _, func in ipairs(functions) do
|
||||||
|
local task = tasks.run(func, function(success, ...)
|
||||||
|
if event.is_set() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
failed = not success
|
||||||
|
result = { ... }
|
||||||
|
event.set()
|
||||||
|
end)
|
||||||
|
table.insert(running_tasks, task)
|
||||||
|
end
|
||||||
|
event.wait()
|
||||||
|
for _, task in ipairs(running_tasks) do
|
||||||
|
task.cancel()
|
||||||
|
end
|
||||||
|
if failed then
|
||||||
|
error(unpack(result))
|
||||||
|
end
|
||||||
|
return unpack(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
local async_defer = nio.wrap(function(time, cb)
|
||||||
|
assert(cb, "Cannot call sleep from non-async context")
|
||||||
|
vim.defer_fn(cb, time)
|
||||||
|
end, 2)
|
||||||
|
|
||||||
|
--- Suspend the current task for given time.
|
||||||
|
---@param ms number Time in milliseconds
|
||||||
|
function nio.sleep(ms)
|
||||||
|
async_defer(ms)
|
||||||
|
end
|
||||||
|
|
||||||
|
local wrapped_schedule = nio.wrap(vim.schedule, 1)
|
||||||
|
|
||||||
|
--- Yields to the Neovim scheduler to be able to call the API.
|
||||||
|
---@async
|
||||||
|
function nio.scheduler()
|
||||||
|
wrapped_schedule()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@nodoc
|
||||||
|
local function proxy_vim(prop)
|
||||||
|
return setmetatable({}, {
|
||||||
|
__index = function(_, k)
|
||||||
|
return function(...)
|
||||||
|
-- if we are in a fast event await the scheduler
|
||||||
|
if vim.in_fast_event() then
|
||||||
|
nio.scheduler()
|
||||||
|
end
|
||||||
|
|
||||||
|
return vim[prop][k](...)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Safely proxies calls to the vim.api module while in an async context.
|
||||||
|
nio.api = proxy_vim("api")
|
||||||
|
--- Safely proxies calls to the vim.fn module while in an async context.
|
||||||
|
nio.fn = proxy_vim("fn")
|
||||||
|
|
||||||
|
-- For type checking
|
||||||
|
if false then
|
||||||
|
nio.api = vim.api
|
||||||
|
nio.fn = vim.fn
|
||||||
|
end
|
||||||
|
|
||||||
|
return nio
|
103
lua/nio/logger.lua
Normal file
103
lua/nio/logger.lua
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
local loggers = {}
|
||||||
|
|
||||||
|
local log_date_format = "%FT%H:%M:%SZ%z"
|
||||||
|
|
||||||
|
---@class nio.Logger
|
||||||
|
---@field trace function
|
||||||
|
---@field debug function
|
||||||
|
---@field info function
|
||||||
|
---@field warn function
|
||||||
|
---@field error function
|
||||||
|
local Logger = {}
|
||||||
|
|
||||||
|
local LARGE = 1e9
|
||||||
|
|
||||||
|
---@return nio.Logger
|
||||||
|
function Logger.new(filename, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local logger = loggers[filename]
|
||||||
|
if logger then
|
||||||
|
return logger
|
||||||
|
end
|
||||||
|
logger = {}
|
||||||
|
setmetatable(logger, { __index = Logger })
|
||||||
|
loggers[filename] = logger
|
||||||
|
local path_sep = (function()
|
||||||
|
if jit then
|
||||||
|
local os = string.lower(jit.os)
|
||||||
|
if os == "linux" or os == "osx" or os == "bsd" then
|
||||||
|
return "/"
|
||||||
|
else
|
||||||
|
return "\\"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return package.config:sub(1, 1)
|
||||||
|
end
|
||||||
|
end)()
|
||||||
|
|
||||||
|
local function path_join(...)
|
||||||
|
return table.concat(vim.tbl_flatten({ ... }), path_sep)
|
||||||
|
end
|
||||||
|
|
||||||
|
logger._level = opts.level or vim.log.levels.WARN
|
||||||
|
local ok, logpath = pcall(vim.fn.stdpath, "log")
|
||||||
|
if not ok then
|
||||||
|
logpath = vim.fn.stdpath("cache")
|
||||||
|
end
|
||||||
|
logger._filename = path_join(logpath, filename .. ".log")
|
||||||
|
|
||||||
|
vim.fn.mkdir(logpath, "p")
|
||||||
|
local logfile = assert(io.open(logger._filename, "a+"))
|
||||||
|
|
||||||
|
local log_info = vim.loop.fs_stat(logger._filename)
|
||||||
|
if log_info and log_info.size > LARGE then
|
||||||
|
local warn_msg =
|
||||||
|
string.format("Nio log is large (%d MB): %s", log_info.size / (1000 * 1000), logger._filename)
|
||||||
|
vim.notify(warn_msg, vim.log.levels.WARN)
|
||||||
|
end
|
||||||
|
|
||||||
|
for level, levelnr in pairs(vim.log.levels) do
|
||||||
|
logger[level:lower()] = function(...)
|
||||||
|
local argc = select("#", ...)
|
||||||
|
if levelnr < logger._level then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if argc == 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local info = debug.getinfo(2, "Sl")
|
||||||
|
local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
|
||||||
|
local parts = {
|
||||||
|
table.concat({ level, "|", os.date(log_date_format), "|", fileinfo, "|" }, " "),
|
||||||
|
}
|
||||||
|
for i = 1, argc do
|
||||||
|
local arg = select(i, ...)
|
||||||
|
if arg == nil then
|
||||||
|
table.insert(parts, "<nil>")
|
||||||
|
elseif type(arg) == "string" then
|
||||||
|
table.insert(parts, arg)
|
||||||
|
elseif type(arg) == "table" and arg.__tostring then
|
||||||
|
table.insert(parts, arg.__tostring(arg))
|
||||||
|
else
|
||||||
|
table.insert(parts, vim.inspect(arg))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
logfile:write(table.concat(parts, " "), "\n")
|
||||||
|
logfile:flush()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return logger
|
||||||
|
end
|
||||||
|
|
||||||
|
function Logger:set_level(level)
|
||||||
|
self._level = assert(
|
||||||
|
type(level) == "number" and level or vim.log.levels[tostring(level):upper()],
|
||||||
|
string.format("Log level must be one of (trace, debug, info, warn, error), got: %q", level)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Logger:get_filename()
|
||||||
|
return self._filename
|
||||||
|
end
|
||||||
|
|
||||||
|
return Logger.new("nio")
|
3024
lua/nio/lsp-types.lua
Normal file
3024
lua/nio/lsp-types.lua
Normal file
File diff suppressed because it is too large
Load diff
112
lua/nio/lsp.lua
Normal file
112
lua/nio/lsp.lua
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
local control = require("nio.control")
|
||||||
|
local logger = require("nio.logger")
|
||||||
|
|
||||||
|
local nio = {}
|
||||||
|
|
||||||
|
---@toc_entry nio.lsp
|
||||||
|
---@class nio.lsp
|
||||||
|
nio.lsp = {}
|
||||||
|
|
||||||
|
---@class nio.lsp.Client
|
||||||
|
---@field request nio.lsp.RequestClient Interface to all requests that can be sent by the client
|
||||||
|
---@field notify nio.lsp.NotifyClient Interface to all notifications that can be sent by the client
|
||||||
|
---@field server_capabilities nio.lsp.types.ServerCapabilities
|
||||||
|
|
||||||
|
local async_request = tasks.wrap(function(client, method, params, bufnr, request_id_future, cb)
|
||||||
|
local success, req_id = client.request(method, params, cb, bufnr)
|
||||||
|
if not success then
|
||||||
|
if request_id_future then
|
||||||
|
request_id_future.set_error("Request failed")
|
||||||
|
end
|
||||||
|
error(("Failed to send request. Client %s has shut down"):format(client.id))
|
||||||
|
end
|
||||||
|
if request_id_future then
|
||||||
|
request_id_future.set(req_id)
|
||||||
|
end
|
||||||
|
end, 6)
|
||||||
|
|
||||||
|
--- Get active clients, optionally matching the given filters
|
||||||
|
--- Equivalent to |vim.lsp.get_clients|
|
||||||
|
---@param filters? nio.lsp.GetClientsFilters
|
||||||
|
---@return nio.lsp.Client[]
|
||||||
|
function nio.lsp.get_clients(filters)
|
||||||
|
local clients = {}
|
||||||
|
for _, client in pairs(vim.lsp.get_active_clients(filters)) do
|
||||||
|
clients[#clients + 1] = nio.lsp.client(client.id)
|
||||||
|
end
|
||||||
|
return clients
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class nio.lsp.GetClientsFilters
|
||||||
|
---@field id? integer
|
||||||
|
---@field name? string
|
||||||
|
---@field bufnr? integer
|
||||||
|
---@field method? string
|
||||||
|
|
||||||
|
---Create an async client for the given client id
|
||||||
|
---@param client_id integer
|
||||||
|
---@return nio.lsp.Client
|
||||||
|
function nio.lsp.client(client_id)
|
||||||
|
local n = require("nio")
|
||||||
|
local internal_client =
|
||||||
|
assert(vim.lsp.get_client_by_id(client_id), ("Client not found with ID %s"):format(client_id))
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
local convert_method = function(name)
|
||||||
|
return name:gsub("__", "$/"):gsub("_", "/")
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
server_capabilities = internal_client.server_capabilities,
|
||||||
|
notify = setmetatable({}, {
|
||||||
|
__index = function(_, method)
|
||||||
|
method = convert_method(method)
|
||||||
|
return function(params)
|
||||||
|
return internal_client.notify(method, params)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
request = setmetatable({}, {
|
||||||
|
__index = function(_, method)
|
||||||
|
method = convert_method(method)
|
||||||
|
---@param opts? nio.lsp.RequestOpts
|
||||||
|
return function(params, bufnr, opts)
|
||||||
|
-- No params for this request
|
||||||
|
if type(params) ~= "table" then
|
||||||
|
opts = bufnr
|
||||||
|
bufnr = params
|
||||||
|
end
|
||||||
|
opts = opts or {}
|
||||||
|
local err, result
|
||||||
|
|
||||||
|
local start = vim.loop.now()
|
||||||
|
if opts.timeout then
|
||||||
|
local req_future = control.future()
|
||||||
|
err, result = n.first({
|
||||||
|
function()
|
||||||
|
n.sleep(opts.timeout)
|
||||||
|
local req_id = req_future.wait()
|
||||||
|
n.run(function()
|
||||||
|
async_request(internal_client, "$/cancelRequest", { requestId = req_id }, bufnr)
|
||||||
|
end)
|
||||||
|
return { code = -1, message = "Request timed out" }
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
return async_request(internal_client, method, params, bufnr, req_future)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
err, result = async_request(internal_client, method, params, bufnr)
|
||||||
|
end
|
||||||
|
local elapsed = vim.loop.now() - start
|
||||||
|
logger.trace("Request", method, "took", elapsed, "ms")
|
||||||
|
|
||||||
|
return err, result
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return nio.lsp
|
212
lua/nio/tasks.lua
Normal file
212
lua/nio/tasks.lua
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
local nio = {}
|
||||||
|
|
||||||
|
---@class nio.tasks
|
||||||
|
nio.tasks = {}
|
||||||
|
|
||||||
|
---@type table<thread, nio.tasks.Task>
|
||||||
|
---@nodoc
|
||||||
|
local tasks = {}
|
||||||
|
---@type table<nio.tasks.Task, nio.tasks.Task[]>
|
||||||
|
---@nodoc
|
||||||
|
local child_tasks = {}
|
||||||
|
|
||||||
|
-- Coroutine.running() was changed between Lua 5.1 and 5.2:
|
||||||
|
-- - 5.1: Returns the running coroutine, or nil when called by the main thread.
|
||||||
|
-- - 5.2: Returns the running coroutine plus a boolean, true when the running coroutine is the main one.
|
||||||
|
-- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
|
||||||
|
|
||||||
|
---@nodoc
|
||||||
|
local function current_non_main_co()
|
||||||
|
local data = { coroutine.running() }
|
||||||
|
|
||||||
|
if select("#", unpack(data)) == 2 then
|
||||||
|
local co, is_main = unpack(data)
|
||||||
|
if is_main then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return co
|
||||||
|
end
|
||||||
|
|
||||||
|
return unpack(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@text
|
||||||
|
--- Tasks represent a top level running asynchronous function
|
||||||
|
--- Only one task is ever executing at any time.
|
||||||
|
---@class nio.tasks.Task
|
||||||
|
---@field parent? nio.tasks.Task Parent task
|
||||||
|
---@field cancel fun(): nil Cancels the task
|
||||||
|
---@field trace fun(): string Get the stack trace of the task
|
||||||
|
---@field wait async function Wait for the task to finish, returning any result
|
||||||
|
|
||||||
|
---@class nio.tasks.TaskError
|
||||||
|
---@field message string
|
||||||
|
---@field traceback? string
|
||||||
|
|
||||||
|
local format_error = function(message, traceback)
|
||||||
|
if not traceback then
|
||||||
|
return string.format("The coroutine failed with this message: %s", message)
|
||||||
|
end
|
||||||
|
return string.format(
|
||||||
|
"The coroutine failed with this message: %s\n%s",
|
||||||
|
type(message) == "string" and vim.startswith(traceback, message) and ""
|
||||||
|
or ("\n" .. tostring(message)),
|
||||||
|
traceback
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return nio.tasks.Task
|
||||||
|
---@nodoc
|
||||||
|
function nio.tasks.run(func, cb)
|
||||||
|
local co = coroutine.create(func)
|
||||||
|
local cancelled = false
|
||||||
|
local task = { parent = nio.tasks.current_task() }
|
||||||
|
if task.parent then
|
||||||
|
child_tasks[task.parent] = child_tasks[task.parent] or {}
|
||||||
|
table.insert(child_tasks[task.parent], task)
|
||||||
|
end
|
||||||
|
local future = require("nio").control.future()
|
||||||
|
|
||||||
|
function task.cancel()
|
||||||
|
if coroutine.status(co) == "dead" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, child in pairs(child_tasks[task] or {}) do
|
||||||
|
child.cancel()
|
||||||
|
end
|
||||||
|
cancelled = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function task.trace()
|
||||||
|
return debug.traceback(co)
|
||||||
|
end
|
||||||
|
|
||||||
|
function task.wait()
|
||||||
|
return future.wait()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function close_task(result, err)
|
||||||
|
tasks[co] = nil
|
||||||
|
if err then
|
||||||
|
future.set_error(err)
|
||||||
|
if cb then
|
||||||
|
cb(false, err)
|
||||||
|
elseif not cancelled then
|
||||||
|
error("Async task failed without callback: " .. err)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
future.set(unpack(result))
|
||||||
|
if cb then
|
||||||
|
cb(true, unpack(result))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tasks[co] = task
|
||||||
|
|
||||||
|
local function step(...)
|
||||||
|
if cancelled then
|
||||||
|
close_task(nil, format_error("Task was cancelled"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local yielded = { coroutine.resume(co, ...) }
|
||||||
|
local success = yielded[1]
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
close_task(nil, format_error(yielded[2], debug.traceback(co)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if coroutine.status(co) == "dead" then
|
||||||
|
close_task({ unpack(yielded, 2, table.maxn(yielded)) })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, nargs, err_or_fn = unpack(yielded)
|
||||||
|
|
||||||
|
if type(err_or_fn) ~= "function" then
|
||||||
|
error(
|
||||||
|
("Async internal error: expected function, got %s\nContext: %s\n%s"):format(
|
||||||
|
type(err_or_fn),
|
||||||
|
vim.inspect(yielded),
|
||||||
|
debug.traceback(co)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local args = { select(4, unpack(yielded)) }
|
||||||
|
|
||||||
|
args[nargs] = step
|
||||||
|
|
||||||
|
err_or_fn(unpack(args, 1, nargs))
|
||||||
|
end
|
||||||
|
|
||||||
|
step()
|
||||||
|
return task
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param func function
|
||||||
|
---@param argc? number
|
||||||
|
---@return function
|
||||||
|
function nio.tasks.create(func, argc)
|
||||||
|
vim.validate({
|
||||||
|
func = { func, "function" },
|
||||||
|
argc = { argc, "number", true },
|
||||||
|
})
|
||||||
|
argc = argc or 0
|
||||||
|
return function(...)
|
||||||
|
if current_non_main_co() then
|
||||||
|
return func(...)
|
||||||
|
end
|
||||||
|
local args = { ... }
|
||||||
|
local callback
|
||||||
|
if #args > argc then
|
||||||
|
callback = table.remove(args)
|
||||||
|
end
|
||||||
|
return nio.tasks.run(function()
|
||||||
|
func(unpack(args))
|
||||||
|
end, callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@package
|
||||||
|
---@nodoc
|
||||||
|
function nio.tasks.wrap(func, argc)
|
||||||
|
vim.validate({ func = { func, "function" }, argc = { argc, "number" } })
|
||||||
|
local protected = function(...)
|
||||||
|
local args = { ... }
|
||||||
|
local cb = args[argc]
|
||||||
|
args[argc] = function(...)
|
||||||
|
cb(true, ...)
|
||||||
|
end
|
||||||
|
xpcall(func, function(err)
|
||||||
|
cb(false, err, debug.traceback())
|
||||||
|
end, unpack(args, 1, argc))
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(...)
|
||||||
|
if not current_non_main_co() then
|
||||||
|
return func(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ret = { coroutine.yield(argc, protected, ...) }
|
||||||
|
local success = ret[1]
|
||||||
|
if not success then
|
||||||
|
error(("Wrapped function failed: %s\n%s"):format(ret[2], ret[3]))
|
||||||
|
end
|
||||||
|
return unpack(ret, 2, table.maxn(ret))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the current running task
|
||||||
|
---@return nio.tasks.Task|nil
|
||||||
|
function nio.tasks.current_task()
|
||||||
|
local co = current_non_main_co()
|
||||||
|
if not co then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return tasks[co]
|
||||||
|
end
|
||||||
|
|
||||||
|
return nio.tasks
|
64
lua/nio/tests.lua
Normal file
64
lua/nio/tests.lua
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
|
||||||
|
local nio = {}
|
||||||
|
|
||||||
|
---@toc_entry nio.tests
|
||||||
|
---@text
|
||||||
|
--- Wrappers around plenary.nvim's test functions for writing async tests
|
||||||
|
--- ```lua
|
||||||
|
--- a.it("notifies listeners", function()
|
||||||
|
--- local event = nio.control.event()
|
||||||
|
--- local notified = 0
|
||||||
|
--- for _ = 1, 10 do
|
||||||
|
--- nio.run(function()
|
||||||
|
--- event.wait()
|
||||||
|
--- notified = notified + 1
|
||||||
|
--- end)
|
||||||
|
--- end
|
||||||
|
---
|
||||||
|
--- event.set()
|
||||||
|
--- nio.sleep(10)
|
||||||
|
--- assert.equals(10, notified)
|
||||||
|
--- end)
|
||||||
|
---@class nio.tests
|
||||||
|
nio.tests = {}
|
||||||
|
|
||||||
|
local with_timeout = function(func, timeout)
|
||||||
|
local success, err
|
||||||
|
return function()
|
||||||
|
local task = tasks.run(func, function(success_, err_)
|
||||||
|
success = success_
|
||||||
|
if not success_ then
|
||||||
|
err = err_
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.wait(timeout or 2000, function()
|
||||||
|
return success ~= nil
|
||||||
|
end, 20, false)
|
||||||
|
|
||||||
|
if success == nil then
|
||||||
|
error(string.format("Test task timed out\n%s", task.trace()))
|
||||||
|
elseif not success then
|
||||||
|
error(string.format("Test task failed with message:\n%s", err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@param async_func function
|
||||||
|
nio.tests.it = function(name, async_func)
|
||||||
|
it(name, with_timeout(async_func, tonumber(vim.env.PLENARY_TEST_TIMEOUT)))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param async_func function
|
||||||
|
nio.tests.before_each = function(async_func)
|
||||||
|
before_each(with_timeout(async_func))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param async_func function
|
||||||
|
nio.tests.after_each = function(async_func)
|
||||||
|
after_each(with_timeout(async_func))
|
||||||
|
end
|
||||||
|
|
||||||
|
return nio.tests
|
50
lua/nio/ui.lua
Normal file
50
lua/nio/ui.lua
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
|
||||||
|
local nio = {}
|
||||||
|
|
||||||
|
---@toc_entry nio.ui
|
||||||
|
---@text
|
||||||
|
--- Async versions of vim.ui functions.
|
||||||
|
---@class nio.ui
|
||||||
|
nio.ui = {}
|
||||||
|
|
||||||
|
--- Prompt the user for input.
|
||||||
|
--- See |vim.ui.input()| for details.
|
||||||
|
--- ```lua
|
||||||
|
--- local value = nio.ui.input({ prompt = "Enter something: " })
|
||||||
|
--- print(("You entered: %s"):format(value))
|
||||||
|
--- ```
|
||||||
|
---@async
|
||||||
|
---@param args nio.ui.InputArgs
|
||||||
|
function nio.ui.input(args) end
|
||||||
|
|
||||||
|
---@class nio.ui.InputArgs
|
||||||
|
---@field prompt string|nil Text of the prompt
|
||||||
|
---@field default string|nil Default reply to the input
|
||||||
|
---@field completion string|nil Specifies type of completion supported for input. Supported types are the same that can be supplied to a user-defined command using the "-complete=" argument. See |:command-completion|
|
||||||
|
---@field highlight function Function that will be used for highlighting user inputs.
|
||||||
|
|
||||||
|
--- Prompts the user to pick from a list of items
|
||||||
|
--- See |vim.ui.select()| for details.
|
||||||
|
--- ```
|
||||||
|
--- local value = nio.ui.select({ "foo", "bar", "baz" }, { prompt = "Select something: " })
|
||||||
|
--- print(("You entered: %s"):format(value))
|
||||||
|
--- ```
|
||||||
|
---@async
|
||||||
|
---@param items any[]
|
||||||
|
---@param args nio.ui.SelectArgs
|
||||||
|
function nio.ui.select(items, args) end
|
||||||
|
|
||||||
|
---@class nio.ui.SelectArgs
|
||||||
|
---@field prompt string|nil Text of the prompt. Defaults to `Select one of:`
|
||||||
|
---@field format_item function|nil Function to format an individual item from `items`. Defaults to `tostring`.
|
||||||
|
---@field kind string|nil Arbitrary hint string indicating the item shape. Plugins reimplementing `vim.ui.select` may wish to use this to infer the structure or semantics of `items`, or the context in which select() was called.
|
||||||
|
|
||||||
|
nio.ui = {
|
||||||
|
---@nodoc
|
||||||
|
select = tasks.wrap(vim.ui.select, 3),
|
||||||
|
---@nodoc
|
||||||
|
input = tasks.wrap(vim.ui.input, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nio.ui
|
169
lua/nio/uv.lua
Normal file
169
lua/nio/uv.lua
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
|
||||||
|
local nio = {}
|
||||||
|
|
||||||
|
---@toc_entry nio.uv
|
||||||
|
---@text
|
||||||
|
--- Provides asynchronous versions of vim.loop functions.
|
||||||
|
--- See corresponding function documentation for parameter and return
|
||||||
|
--- information.
|
||||||
|
--- ```lua
|
||||||
|
--- local file_path = "README.md"
|
||||||
|
---
|
||||||
|
--- local open_err, file_fd = nio.uv.fs_open(file_path, "r", 438)
|
||||||
|
--- assert(not open_err, open_err)
|
||||||
|
---
|
||||||
|
--- local stat_err, stat = nio.uv.fs_fstat(file_fd)
|
||||||
|
--- assert(not stat_err, stat_err)
|
||||||
|
---
|
||||||
|
--- local read_err, data = nio.uv.fs_read(file_fd, stat.size, 0)
|
||||||
|
--- assert(not read_err, read_err)
|
||||||
|
---
|
||||||
|
--- local close_err = nio.uv.fs_close(file_fd)
|
||||||
|
--- assert(not close_err, close_err)
|
||||||
|
---
|
||||||
|
--- print(data)
|
||||||
|
--- ```
|
||||||
|
---
|
||||||
|
---@class nio.uv
|
||||||
|
---@field close async fun(handle: nio.uv.Handle)
|
||||||
|
---@field fs_open async fun(path: any, flags: any, mode: any): (string|nil,integer|nil)
|
||||||
|
---@field fs_read async fun(fd: integer, size: integer, offset?: integer): (string|nil,string|nil)
|
||||||
|
---@field fs_close async fun(fd: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_unlink async fun(path: string): (string|nil,boolean|nil)
|
||||||
|
---@field fs_write async fun(fd: any, data: any, offset?: any): (string|nil,integer|nil)
|
||||||
|
---@field fs_mkdir async fun(path: string, mode: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_mkdtemp async fun(template: string): (string|nil,string|nil)
|
||||||
|
---@field fs_rmdir async fun(path: string): (string|nil,boolean|nil)
|
||||||
|
---@field fs_stat async fun(path: string): (string|nil,nio.uv.Stat|nil)
|
||||||
|
---@field fs_fstat async fun(fd: integer): (string|nil,nio.uv.Stat|nil)
|
||||||
|
---@field fs_lstat async fun(path: string): (string|nil,nio.uv.Stat|nil)
|
||||||
|
---@field fs_statfs async fun(path: string): (string|nil,nio.uv.StatFs|nil)
|
||||||
|
---@field fs_rename async fun(old_path: string, new_path: string): (string|nil,boolean|nil)
|
||||||
|
---@field fs_fsync async fun(fd: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_fdatasync async fun(fd: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_ftruncate async fun(fd: integer, offset: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_sendfile async fun(out_fd: integer, in_fd: integer, in_offset: integer, length: integer): (string|nil,integer|nil)
|
||||||
|
---@field fs_access async fun(path: string, mode: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_chmod async fun(path: string, mode: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_fchmod async fun(fd: integer, mode: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_utime async fun(path: string, atime: number, mtime: number): (string|nil,boolean|nil)
|
||||||
|
---@field fs_futime async fun(fd: integer, atime: number, mtime: number): (string|nil,boolean|nil)
|
||||||
|
---@field fs_link async fun(path: string, new_path: string): (string|nil,boolean|nil)
|
||||||
|
---@field fs_symlink async fun(path: string, new_path: string, flags?: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_readlink async fun(path: string): (string|nil,string|nil)
|
||||||
|
---@field fs_realpath async fun(path: string): (string|nil,string|nil)
|
||||||
|
---@field fs_chown async fun(path: string, uid: integer, gid: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_fchown async fun(fd: integer, uid: integer, gid: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_lchown async fun(path: string, uid: integer, gid: integer): (string|nil,boolean|nil)
|
||||||
|
---@field fs_copyfile async fun(path: any, new_path: any, flags?: any): (string|nil,boolean|nil)
|
||||||
|
---@field fs_opendir async fun(path: string, entries?: integer): (string|nil,nio.uv.Dir|nil)
|
||||||
|
---@field fs_readdir async fun(dir: nio.uv.Dir): (string|nil,nio.uv.DirEntry[]|nil)
|
||||||
|
---@field fs_closedir async fun(dir: nio.uv.Dir): (string|nil,boolean|nil)
|
||||||
|
---@field fs_scandir async fun(path: string): (string|nil,nio.uv.DirEntry[]|nil)
|
||||||
|
---@field shutdown async fun(stream: nio.uv.Stream): string|nil
|
||||||
|
---@field listen async fun(stream: nio.uv.Stream, backlog: integer): string|nil
|
||||||
|
---@field write async fun(stream: nio.uv.Stream, data: string|string[]): string|nil
|
||||||
|
---@field write2 async fun(stream: nio.uv.Stream, data: string|string[], send_handle: nio.uv.Stream): string|nil
|
||||||
|
nio.uv = {}
|
||||||
|
|
||||||
|
---@class nio.uv.Handle
|
||||||
|
|
||||||
|
---@class nio.uv.Stream : nio.uv.Handle
|
||||||
|
|
||||||
|
---@class nio.uv.Stat
|
||||||
|
---@field dev integer
|
||||||
|
---@field mode integer
|
||||||
|
---@field nlink integer
|
||||||
|
---@field uid integer
|
||||||
|
---@field gid integer
|
||||||
|
---@field rdev integer
|
||||||
|
---@field ino integer
|
||||||
|
---@field size integer
|
||||||
|
---@field blksize integer
|
||||||
|
---@field blocks integer
|
||||||
|
---@field flags integer
|
||||||
|
---@field gen integer
|
||||||
|
---@field atime nio.uv.StatTime
|
||||||
|
---@field mtime nio.uv.StatTime
|
||||||
|
---@field ctime nio.uv.StatTime
|
||||||
|
---@field birthtime nio.uv.StatTime
|
||||||
|
---@field type string
|
||||||
|
|
||||||
|
---@class nio.uv.StatTime
|
||||||
|
---@field sec integer
|
||||||
|
---@field nsec integer
|
||||||
|
|
||||||
|
---@class nio.uv.StatFs
|
||||||
|
---@field type integer
|
||||||
|
---@field bsize integer
|
||||||
|
---@field blocks integer
|
||||||
|
---@field bfree integer
|
||||||
|
---@field bavail integer
|
||||||
|
---@field files integer
|
||||||
|
---@field ffree integer
|
||||||
|
|
||||||
|
---@class nio.uv.Dir
|
||||||
|
|
||||||
|
---@class nio.uv.DirEntry
|
||||||
|
|
||||||
|
---@nodoc
|
||||||
|
local function add(name, argc)
|
||||||
|
local success, ret = pcall(tasks.wrap, vim.loop[name], argc)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
error("Failed to add function with name " .. name)
|
||||||
|
end
|
||||||
|
|
||||||
|
nio.uv[name] = ret
|
||||||
|
end
|
||||||
|
|
||||||
|
add("close", 4) -- close a handle
|
||||||
|
-- filesystem operations
|
||||||
|
add("fs_open", 4)
|
||||||
|
add("fs_read", 4)
|
||||||
|
add("fs_close", 2)
|
||||||
|
add("fs_unlink", 2)
|
||||||
|
add("fs_write", 4)
|
||||||
|
add("fs_mkdir", 3)
|
||||||
|
add("fs_mkdtemp", 2)
|
||||||
|
-- 'fs_mkstemp',
|
||||||
|
add("fs_rmdir", 2)
|
||||||
|
add("fs_scandir", 2)
|
||||||
|
add("fs_stat", 2)
|
||||||
|
add("fs_fstat", 2)
|
||||||
|
add("fs_lstat", 2)
|
||||||
|
add("fs_rename", 3)
|
||||||
|
add("fs_fsync", 2)
|
||||||
|
add("fs_fdatasync", 2)
|
||||||
|
add("fs_ftruncate", 3)
|
||||||
|
add("fs_sendfile", 5)
|
||||||
|
add("fs_access", 3)
|
||||||
|
add("fs_chmod", 3)
|
||||||
|
add("fs_fchmod", 3)
|
||||||
|
add("fs_utime", 4)
|
||||||
|
add("fs_futime", 4)
|
||||||
|
-- 'fs_lutime',
|
||||||
|
add("fs_link", 3)
|
||||||
|
add("fs_symlink", 4)
|
||||||
|
add("fs_readlink", 2)
|
||||||
|
add("fs_realpath", 2)
|
||||||
|
add("fs_chown", 4)
|
||||||
|
add("fs_fchown", 4)
|
||||||
|
-- 'fs_lchown',
|
||||||
|
add("fs_copyfile", 4)
|
||||||
|
nio.uv.fs_opendir = tasks.wrap(function(path, entries, cb)
|
||||||
|
vim.loop.fs_opendir(path, cb, entries)
|
||||||
|
end, 3)
|
||||||
|
add("fs_readdir", 2)
|
||||||
|
add("fs_closedir", 2)
|
||||||
|
add("fs_statfs", 2)
|
||||||
|
-- stream
|
||||||
|
add("shutdown", 2)
|
||||||
|
add("listen", 3)
|
||||||
|
-- add('read_start', 2) -- do not do this one, the callback is made multiple times
|
||||||
|
add("write", 3)
|
||||||
|
add("write2", 4)
|
||||||
|
add("shutdown", 2)
|
||||||
|
|
||||||
|
return nio.uv
|
3
scripts/docgen
Executable file
3
scripts/docgen
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
nvim --headless -c "luafile ./scripts/gendocs.lua" -c 'qa'
|
854
scripts/gendocs.lua
Normal file
854
scripts/gendocs.lua
Normal file
|
@ -0,0 +1,854 @@
|
||||||
|
-- TODO: A lot of this is private code from minidoc, which could be removed if made public
|
||||||
|
|
||||||
|
local minidoc = require("mini.doc")
|
||||||
|
|
||||||
|
local H = {}
|
||||||
|
--stylua: ignore start
|
||||||
|
H.pattern_sets = {
|
||||||
|
-- Patterns for working with afterlines. At the moment deliberately crafted
|
||||||
|
-- to work only on first line without indent.
|
||||||
|
|
||||||
|
-- Determine if line is a function definition. Captures function name and
|
||||||
|
-- arguments. For reference see '2.5.9 – Function Definitions' in Lua manual.
|
||||||
|
afterline_fundef = {
|
||||||
|
'^function%s+(%S-)(%b())', -- Regular definition
|
||||||
|
'^local%s+function%s+(%S-)(%b())', -- Local definition
|
||||||
|
'^(%S+)%s*=%s*function(%b())', -- Regular assignment
|
||||||
|
'^(%S+)%s*=%s*nio.create%(function(%b())', -- Regular assignment
|
||||||
|
'^local%s+(%S+)%s*=%s*function(%b())', -- Local assignment
|
||||||
|
},
|
||||||
|
-- Determine if line is a general assignment
|
||||||
|
afterline_assign = {
|
||||||
|
'^(%S-)%s*=', -- General assignment
|
||||||
|
'^local%s+(%S-)%s*=', -- Local assignment
|
||||||
|
},
|
||||||
|
-- Patterns to work with type descriptions
|
||||||
|
-- (see https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type)
|
||||||
|
types = {
|
||||||
|
'table%b<>',
|
||||||
|
'fun%b(): %S+', 'fun%b()', 'async fun%b(): %S+', 'async fun%b()',
|
||||||
|
'nil', 'any', 'boolean', 'string', 'number', 'integer', 'function', 'table', 'thread', 'userdata', 'lightuserdata',
|
||||||
|
'%.%.%.',
|
||||||
|
"%S+",
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
H.apply_config = function(config)
|
||||||
|
MiniDoc.config = config
|
||||||
|
end
|
||||||
|
|
||||||
|
H.is_disabled = function()
|
||||||
|
return vim.g.minidoc_disable == true or vim.b.minidoc_disable == true
|
||||||
|
end
|
||||||
|
|
||||||
|
H.get_config = function(config)
|
||||||
|
return vim.tbl_deep_extend("force", MiniDoc.config, vim.b.minidoc_config or {}, config or {})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Work with project specific script ==========================================
|
||||||
|
H.execute_project_script = function(input, output, config)
|
||||||
|
-- Don't process script if there are more than one active `generate` calls
|
||||||
|
if H.generate_is_active then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Don't process script if at least one argument is not default
|
||||||
|
if not (input == nil and output == nil and config == nil) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Store information
|
||||||
|
local global_config_cache = vim.deepcopy(MiniDoc.config)
|
||||||
|
local local_config_cache = vim.b.minidoc_config
|
||||||
|
|
||||||
|
-- Pass information to a possible `generate()` call inside script
|
||||||
|
H.generate_is_active = true
|
||||||
|
H.generate_recent_output = nil
|
||||||
|
|
||||||
|
-- Execute script
|
||||||
|
local success = pcall(vim.cmd, "luafile " .. H.get_config(config).script_path)
|
||||||
|
|
||||||
|
-- Restore information
|
||||||
|
MiniDoc.config = global_config_cache
|
||||||
|
vim.b.minidoc_config = local_config_cache
|
||||||
|
H.generate_is_active = nil
|
||||||
|
|
||||||
|
return success
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default documentation targets ----------------------------------------------
|
||||||
|
H.default_input = function()
|
||||||
|
-- Search in current and recursively in other directories for files with
|
||||||
|
-- 'lua' extension
|
||||||
|
local res = {}
|
||||||
|
for _, dir_glob in ipairs({ ".", "lua/**", "after/**", "colors/**" }) do
|
||||||
|
local files = vim.fn.globpath(dir_glob, "*.lua", false, true)
|
||||||
|
|
||||||
|
-- Use full paths
|
||||||
|
files = vim.tbl_map(function(x)
|
||||||
|
return vim.fn.fnamemodify(x, ":p")
|
||||||
|
end, files)
|
||||||
|
|
||||||
|
-- Put 'init.lua' first among files from same directory
|
||||||
|
table.sort(files, function(a, b)
|
||||||
|
if vim.fn.fnamemodify(a, ":h") == vim.fn.fnamemodify(b, ":h") then
|
||||||
|
if vim.fn.fnamemodify(a, ":t") == "init.lua" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if vim.fn.fnamemodify(b, ":t") == "init.lua" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return a < b
|
||||||
|
end)
|
||||||
|
table.insert(res, files)
|
||||||
|
end
|
||||||
|
|
||||||
|
return vim.tbl_flatten(res)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parsing --------------------------------------------------------------------
|
||||||
|
H.lines_to_block_arr = function(lines, config)
|
||||||
|
local matched_prev, matched_cur
|
||||||
|
|
||||||
|
local res = {}
|
||||||
|
local block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = 1 }
|
||||||
|
|
||||||
|
for i, l in ipairs(lines) do
|
||||||
|
local from, to, section_id = config.annotation_extractor(l)
|
||||||
|
matched_prev, matched_cur = matched_cur, from ~= nil
|
||||||
|
|
||||||
|
if matched_cur then
|
||||||
|
if not matched_prev then
|
||||||
|
-- Finish current block
|
||||||
|
block_raw.line_end = i - 1
|
||||||
|
table.insert(res, H.raw_block_to_block(block_raw, config))
|
||||||
|
|
||||||
|
-- Start new block
|
||||||
|
block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = i }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add annotation line without matched annotation pattern
|
||||||
|
table.insert(block_raw.annotation, ("%s%s"):format(l:sub(0, from - 1), l:sub(to + 1)))
|
||||||
|
|
||||||
|
-- Add section id (it is empty string in case of no section id capture)
|
||||||
|
table.insert(block_raw.section_id, section_id or "")
|
||||||
|
else
|
||||||
|
-- Add afterline
|
||||||
|
table.insert(block_raw.afterlines, l)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
block_raw.line_end = #lines
|
||||||
|
table.insert(res, H.raw_block_to_block(block_raw, config))
|
||||||
|
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Raw block structure is an intermediate step added for convenience. It is
|
||||||
|
-- a table with the following keys:
|
||||||
|
-- - `annotation` - lines (after removing matched annotation pattern) that were
|
||||||
|
-- parsed as annotation.
|
||||||
|
-- - `section_id` - array with length equal to `annotation` length with strings
|
||||||
|
-- captured as section id. Empty string of no section id was captured.
|
||||||
|
-- - Everything else is used as block info (like `afterlines`, etc.).
|
||||||
|
H.raw_block_to_block = function(block_raw, config)
|
||||||
|
if #block_raw.annotation == 0 and #block_raw.afterlines == 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local block = H.new_struct("block", {
|
||||||
|
afterlines = block_raw.afterlines,
|
||||||
|
line_begin = block_raw.line_begin,
|
||||||
|
line_end = block_raw.line_end,
|
||||||
|
})
|
||||||
|
local block_begin = block.info.line_begin
|
||||||
|
|
||||||
|
-- Parse raw block annotation lines from top to bottom. New section starts
|
||||||
|
-- when section id is detected in that line.
|
||||||
|
local section_cur = H.new_struct(
|
||||||
|
"section",
|
||||||
|
{ id = config.default_section_id, line_begin = block_begin }
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, annotation_line in ipairs(block_raw.annotation) do
|
||||||
|
local id = block_raw.section_id[i]
|
||||||
|
if id ~= "" then
|
||||||
|
-- Finish current section
|
||||||
|
if #section_cur > 0 then
|
||||||
|
section_cur.info.line_end = block_begin + i - 2
|
||||||
|
block:insert(section_cur)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Start new section
|
||||||
|
section_cur = H.new_struct("section", { id = id, line_begin = block_begin + i - 1 })
|
||||||
|
end
|
||||||
|
|
||||||
|
section_cur:insert(annotation_line)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #section_cur > 0 then
|
||||||
|
section_cur.info.line_end = block_begin + #block_raw.annotation - 1
|
||||||
|
block:insert(section_cur)
|
||||||
|
end
|
||||||
|
|
||||||
|
return block
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hooks ----------------------------------------------------------------------
|
||||||
|
H.apply_structure_hooks = function(doc, hooks)
|
||||||
|
for _, file in ipairs(doc) do
|
||||||
|
for _, block in ipairs(file) do
|
||||||
|
hooks.block_pre(block)
|
||||||
|
|
||||||
|
for _, section in ipairs(block) do
|
||||||
|
hooks.section_pre(section)
|
||||||
|
|
||||||
|
local hook = hooks.sections[section.info.id]
|
||||||
|
if hook ~= nil then
|
||||||
|
hook(section)
|
||||||
|
end
|
||||||
|
|
||||||
|
hooks.section_post(section)
|
||||||
|
end
|
||||||
|
|
||||||
|
hooks.block_post(block)
|
||||||
|
end
|
||||||
|
|
||||||
|
hooks.file(file)
|
||||||
|
end
|
||||||
|
|
||||||
|
hooks.doc(doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
H.alias_register = function(s)
|
||||||
|
if #s == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove first word (with bits of surrounding whitespace) while capturing it
|
||||||
|
local alias_name
|
||||||
|
s[1] = s[1]:gsub("%s*(%S+) ?", function(x)
|
||||||
|
alias_name = x
|
||||||
|
return ""
|
||||||
|
end, 1)
|
||||||
|
if alias_name == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
MiniDoc.current.aliases = MiniDoc.current.aliases or {}
|
||||||
|
MiniDoc.current.aliases[alias_name] = table.concat(s, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
H.alias_replace = function(s)
|
||||||
|
if MiniDoc.current.aliases == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, _ in ipairs(s) do
|
||||||
|
for alias_name, alias_desc in pairs(MiniDoc.current.aliases) do
|
||||||
|
-- Escape special characters. This is done here and not while registering
|
||||||
|
-- alias to allow user to refer to aliases by its original name.
|
||||||
|
-- Store escaped words in separate variables because `vim.pesc()` returns
|
||||||
|
-- two values which might conflict if outputs are used as arguments.
|
||||||
|
local name_escaped = vim.pesc(alias_name)
|
||||||
|
local desc_escaped = vim.pesc(alias_desc)
|
||||||
|
s[i] = s[i]:gsub(name_escaped, desc_escaped)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
H.toc_register = function(s)
|
||||||
|
MiniDoc.current.toc = MiniDoc.current.toc or {}
|
||||||
|
table.insert(MiniDoc.current.toc, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
H.toc_insert = function(s)
|
||||||
|
if MiniDoc.current.toc == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Render table of contents
|
||||||
|
local toc_lines = {}
|
||||||
|
for _, toc_entry in ipairs(MiniDoc.current.toc) do
|
||||||
|
local _, tag_section = toc_entry.parent:has_descendant(function(x)
|
||||||
|
return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
|
||||||
|
end)
|
||||||
|
tag_section = tag_section or {}
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for i = 1, math.max(#toc_entry, #tag_section) do
|
||||||
|
local left = toc_entry[i] or ""
|
||||||
|
-- Use tag refernce instead of tag enclosure
|
||||||
|
local right = string.match(tag_section[i], "%*.*%*"):gsub("%*", "|")
|
||||||
|
-- local right = vim.trim((tag_section[i] or ""):gsub("%*", "|"))
|
||||||
|
-- Add visual line only at first entry (while not adding trailing space)
|
||||||
|
local filler = i == 1 and "." or (right == "" and "" or " ")
|
||||||
|
-- Make padding of 2 spaces at both left and right
|
||||||
|
local n_filler = math.max(74 - H.visual_text_width(left) - H.visual_text_width(right), 3)
|
||||||
|
table.insert(lines, (" %s%s%s"):format(left, filler:rep(n_filler), right))
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(toc_lines, lines)
|
||||||
|
|
||||||
|
-- Don't show `toc_entry` lines in output
|
||||||
|
toc_entry:clear_lines()
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, l in ipairs(vim.tbl_flatten(toc_lines)) do
|
||||||
|
s:insert(l)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
H.add_section_heading = function(s, heading)
|
||||||
|
if #s == 0 or s.type ~= "section" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add heading
|
||||||
|
s:insert(1, ("%s~"):format(heading))
|
||||||
|
end
|
||||||
|
|
||||||
|
H.enclose_var_name = function(s)
|
||||||
|
if #s == 0 or s.type ~= "section" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
s[1] = s[1]:gsub("(%S+)", "{%1}", 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param init number Start of searching for first "type-like" string. It is
|
||||||
|
--- needed to not detect type early. Like in `@param a_function function`.
|
||||||
|
---@private
|
||||||
|
H.enclose_type = function(s, enclosure, init)
|
||||||
|
if #s == 0 or s.type ~= "section" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
enclosure = enclosure or "`%(%1%)`"
|
||||||
|
init = init or 1
|
||||||
|
|
||||||
|
local cur_type = H.match_first_pattern(s[1], H.pattern_sets["types"], init)
|
||||||
|
if #cur_type == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add `%S*` to front and back of found pattern to support their combination
|
||||||
|
-- with `|`. Also allows using `[]` and `?` prefixes.
|
||||||
|
local type_pattern = ("(%%S*%s%%S*)"):format(vim.pesc(cur_type[1]))
|
||||||
|
|
||||||
|
-- Avoid replacing possible match before `init`
|
||||||
|
local l_start = s[1]:sub(1, init - 1)
|
||||||
|
local l_end = s[1]:sub(init):gsub(type_pattern, enclosure, 1)
|
||||||
|
s[1] = ("%s%s"):format(l_start, l_end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Infer data from afterlines -------------------------------------------------
|
||||||
|
H.infer_header = function(b)
|
||||||
|
local has_signature = b:has_descendant(function(x)
|
||||||
|
return type(x) == "table" and x.type == "section" and x.info.id == "@signature"
|
||||||
|
end)
|
||||||
|
local has_tag = b:has_descendant(function(x)
|
||||||
|
return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
|
||||||
|
end)
|
||||||
|
|
||||||
|
if has_signature and has_tag then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local l_all = table.concat(b.info.afterlines, " ")
|
||||||
|
local tag, signature
|
||||||
|
|
||||||
|
-- Try function definition
|
||||||
|
local fun_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_fundef"])
|
||||||
|
if #fun_capture > 0 then
|
||||||
|
tag = tag or ("%s()"):format(fun_capture[1])
|
||||||
|
signature = signature or ("%s%s"):format(fun_capture[1], fun_capture[2])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Try general assignment
|
||||||
|
local assign_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_assign"])
|
||||||
|
if #assign_capture > 0 then
|
||||||
|
tag = tag or assign_capture[1]
|
||||||
|
signature = signature or assign_capture[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
if tag ~= nil then
|
||||||
|
-- First insert signature (so that it will appear after tag section)
|
||||||
|
if not has_signature then
|
||||||
|
b:insert(1, H.as_struct({ signature }, "section", { id = "@signature" }))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Insert tag
|
||||||
|
if not has_tag then
|
||||||
|
b:insert(1, H.as_struct({ tag }, "section", { id = "@tag" }))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function H.is_module(name)
|
||||||
|
if string.find(name, "%(") then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if string.find(name, "[A-Z]") then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
H.format_signature = function(line)
|
||||||
|
-- Try capture function signature
|
||||||
|
local name, args = line:match("(%S-)(%b())")
|
||||||
|
|
||||||
|
|
||||||
|
-- Otherwise pick first word
|
||||||
|
name = name or line:match("(%S+)")
|
||||||
|
if not args and H.is_module(name) then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
local name_elems = vim.split(name, ".", { plain = true })
|
||||||
|
name = name_elems[#name_elems]
|
||||||
|
|
||||||
|
if not name then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tidy arguments
|
||||||
|
if args and args ~= "()" then
|
||||||
|
local arg_parts = vim.split(args:sub(2, -2), ",")
|
||||||
|
local arg_list = {}
|
||||||
|
for _, a in ipairs(arg_parts) do
|
||||||
|
-- Enclose argument in `{}` while controlling whitespace
|
||||||
|
table.insert(arg_list, ("{%s}"):format(vim.trim(a)))
|
||||||
|
end
|
||||||
|
args = ("(%s)"):format(table.concat(arg_list, ", "))
|
||||||
|
end
|
||||||
|
|
||||||
|
return ("`%s`%s"):format(name, args or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Work with structures -------------------------------------------------------
|
||||||
|
-- Constructor
|
||||||
|
H.new_struct = function(struct_type, info)
|
||||||
|
local output = {
|
||||||
|
info = info or {},
|
||||||
|
type = struct_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
output.insert = function(self, index, child)
|
||||||
|
-- Allow both `x:insert(child)` and `x:insert(1, child)`
|
||||||
|
if child == nil then
|
||||||
|
child, index = index, #self + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(child) == "table" then
|
||||||
|
child.parent = self
|
||||||
|
child.parent_index = index
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(self, index, child)
|
||||||
|
|
||||||
|
H.sync_parent_index(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
output.remove = function(self, index)
|
||||||
|
index = index or #self
|
||||||
|
table.remove(self, index)
|
||||||
|
|
||||||
|
H.sync_parent_index(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
output.has_descendant = function(self, predicate)
|
||||||
|
local bool_res, descendant = false, nil
|
||||||
|
H.apply_recursively(function(x)
|
||||||
|
if not bool_res and predicate(x) then
|
||||||
|
bool_res = true
|
||||||
|
descendant = x
|
||||||
|
end
|
||||||
|
end, self)
|
||||||
|
return bool_res, descendant
|
||||||
|
end
|
||||||
|
|
||||||
|
output.has_lines = function(self)
|
||||||
|
return self:has_descendant(function(x)
|
||||||
|
return type(x) == "string"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
output.clear_lines = function(self)
|
||||||
|
for i, x in ipairs(self) do
|
||||||
|
if type(x) == "string" then
|
||||||
|
self[i] = nil
|
||||||
|
else
|
||||||
|
x:clear_lines()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
H.sync_parent_index = function(x)
|
||||||
|
for i, _ in ipairs(x) do
|
||||||
|
if type(x[i]) == "table" then
|
||||||
|
x[i].parent_index = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return x
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Converter (this ensures that children have proper parent-related data)
|
||||||
|
H.as_struct = function(array, struct_type, info)
|
||||||
|
-- Make default info `info` for cases when structure is created manually
|
||||||
|
local default_info = ({
|
||||||
|
section = { id = "@text", line_begin = -1, line_end = -1 },
|
||||||
|
block = { afterlines = {}, line_begin = -1, line_end = -1 },
|
||||||
|
file = { path = "" },
|
||||||
|
doc = { input = {}, output = "", config = H.get_config() },
|
||||||
|
})[struct_type]
|
||||||
|
info = vim.tbl_deep_extend("force", default_info, info or {})
|
||||||
|
|
||||||
|
local res = H.new_struct(struct_type, info)
|
||||||
|
for _, x in ipairs(array) do
|
||||||
|
res:insert(x)
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Work with text -------------------------------------------------------------
|
||||||
|
H.ensure_indent = function(text, n_indent_target)
|
||||||
|
local lines = vim.split(text, "\n")
|
||||||
|
local n_indent, n_indent_cur = math.huge, math.huge
|
||||||
|
|
||||||
|
-- Find number of characters in indent
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
-- Update lines indent: minimum of all indents except empty lines
|
||||||
|
if n_indent > 0 then
|
||||||
|
_, n_indent_cur = l:find("^%s*")
|
||||||
|
-- Condition "current n-indent equals line length" detects empty line
|
||||||
|
if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then
|
||||||
|
n_indent = n_indent_cur
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure indent
|
||||||
|
local indent = string.rep(" ", n_indent_target)
|
||||||
|
for i, l in ipairs(lines) do
|
||||||
|
if l ~= "" then
|
||||||
|
lines[i] = indent .. l:sub(n_indent + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(lines, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
H.align_text = function(text, width, direction)
|
||||||
|
if type(text) ~= "string" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
text = vim.trim(text)
|
||||||
|
width = width or 78
|
||||||
|
direction = direction or "left"
|
||||||
|
|
||||||
|
-- Don't do anything if aligning left or line is a whitespace
|
||||||
|
if direction == "left" or text:find("^%s*$") then
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
local n_left = math.max(0, 78 - H.visual_text_width(text))
|
||||||
|
if direction == "center" then
|
||||||
|
n_left = math.floor(0.5 * n_left)
|
||||||
|
end
|
||||||
|
|
||||||
|
return (" "):rep(n_left) .. text
|
||||||
|
end
|
||||||
|
|
||||||
|
H.visual_text_width = function(text)
|
||||||
|
-- Ignore concealed characters (usually "invisible" in 'help' filetype)
|
||||||
|
local _, n_concealed_chars = text:gsub("([*|`])", "%1")
|
||||||
|
return vim.fn.strdisplaywidth(text) - n_concealed_chars
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Return earliest match among many patterns
|
||||||
|
---
|
||||||
|
--- Logic here is to test among several patterns. If several got a match,
|
||||||
|
--- return one with earliest match.
|
||||||
|
---
|
||||||
|
---@private
|
||||||
|
H.match_first_pattern = function(text, pattern_set, init)
|
||||||
|
local start_tbl = vim.tbl_map(function(pattern)
|
||||||
|
return text:find(pattern, init) or math.huge
|
||||||
|
end, pattern_set)
|
||||||
|
|
||||||
|
local min_start, min_id = math.huge, nil
|
||||||
|
for id, st in ipairs(start_tbl) do
|
||||||
|
if st < min_start then
|
||||||
|
min_start, min_id = st, id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if min_id == nil then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return { text:match(pattern_set[min_id], init) }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Utilities ------------------------------------------------------------------
|
||||||
|
H.apply_recursively = function(f, x, used)
|
||||||
|
used = used or {}
|
||||||
|
if used[x] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
f(x)
|
||||||
|
used[x] = true
|
||||||
|
|
||||||
|
if type(x) == "table" then
|
||||||
|
for _, t in ipairs(x) do
|
||||||
|
H.apply_recursively(f, t, used)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
H.collect_strings = function(x)
|
||||||
|
local res = {}
|
||||||
|
H.apply_recursively(function(y)
|
||||||
|
if type(y) == "string" then
|
||||||
|
-- Allow `\n` in strings
|
||||||
|
table.insert(res, vim.split(y, "\n"))
|
||||||
|
end
|
||||||
|
end, x)
|
||||||
|
-- Flatten to only have strings and not table of strings (from `vim.split`)
|
||||||
|
return vim.tbl_flatten(res)
|
||||||
|
end
|
||||||
|
|
||||||
|
H.file_read = function(path)
|
||||||
|
local file = assert(io.open(path))
|
||||||
|
local contents = file:read("*all")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
return vim.split(contents, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
H.file_write = function(path, lines)
|
||||||
|
-- Ensure target directory exists
|
||||||
|
local dir = vim.fn.fnamemodify(path, ":h")
|
||||||
|
vim.fn.mkdir(dir, "p")
|
||||||
|
|
||||||
|
-- Write to file
|
||||||
|
vim.fn.writefile(lines, path, "b")
|
||||||
|
end
|
||||||
|
|
||||||
|
H.full_path = function(path)
|
||||||
|
return vim.fn.resolve(vim.fn.fnamemodify(path, ":p"))
|
||||||
|
end
|
||||||
|
|
||||||
|
H.message = function(msg)
|
||||||
|
vim.cmd("echomsg " .. vim.inspect("(mini.doc) " .. msg))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrap(str, limit, indent, indent1)
|
||||||
|
indent = indent or ""
|
||||||
|
indent1 = indent1 or indent
|
||||||
|
limit = limit or 79
|
||||||
|
local here = 1 - #indent1
|
||||||
|
local wrapped = indent1
|
||||||
|
.. str:gsub("(%s+)()(%S+)()", function(sp, st, word, fi)
|
||||||
|
local delta = 0
|
||||||
|
word:gsub("@([@%a])", function(c)
|
||||||
|
if c == "@" then
|
||||||
|
delta = delta + 1
|
||||||
|
elseif c == "x" then
|
||||||
|
delta = delta + 5
|
||||||
|
else
|
||||||
|
delta = delta + 2
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
here = here + delta
|
||||||
|
if fi - here > limit then
|
||||||
|
here = st - #indent + delta
|
||||||
|
return "\n" .. indent .. word
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return vim.split(wrapped, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function create_config(module, header)
|
||||||
|
return {
|
||||||
|
hooks = vim.tbl_extend("force", minidoc.default_hooks, {
|
||||||
|
block_pre = function(b)
|
||||||
|
-- Infer metadata based on afterlines
|
||||||
|
if b:has_lines() and #b.info.afterlines > 0 then H.infer_header(b) end
|
||||||
|
end,
|
||||||
|
section_post = function(section)
|
||||||
|
for i, line in ipairs(section) do
|
||||||
|
if type(line) == "string" then
|
||||||
|
if string.find(line, "^```") then
|
||||||
|
string.gsub(line, "```(.*)", function(lang)
|
||||||
|
section[i] = lang == "" and "<" or (">%s"):format(lang)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
block_post = function(b)
|
||||||
|
if not b:has_lines() then return end
|
||||||
|
|
||||||
|
local found_param, found_field = false, false
|
||||||
|
local n_tag_sections = 0
|
||||||
|
H.apply_recursively(function(x)
|
||||||
|
if not (type(x) == 'table' and x.type == 'section') then return end
|
||||||
|
|
||||||
|
-- Add headings before first occurence of a section which type usually
|
||||||
|
-- appear several times
|
||||||
|
if not found_param and x.info.id == '@param' then
|
||||||
|
H.add_section_heading(x, 'Parameters')
|
||||||
|
found_param = true
|
||||||
|
end
|
||||||
|
if not found_field and x.info.id == '@field' then
|
||||||
|
H.add_section_heading(x, 'Fields')
|
||||||
|
found_field = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if x.info.id == '@tag' then
|
||||||
|
local text = x[1]
|
||||||
|
local tag = string.match(text, "%*.*%*")
|
||||||
|
local prefix = (string.sub(tag, 2, #tag - 1))
|
||||||
|
if not H.is_module(prefix) then
|
||||||
|
prefix = ""
|
||||||
|
end
|
||||||
|
local n_filler = math.max(78 - H.visual_text_width(prefix) - H.visual_text_width(tag), 3)
|
||||||
|
local line = ("%s%s%s"):format(prefix, (" "):rep(n_filler), tag)
|
||||||
|
x:remove(1)
|
||||||
|
x:insert(1, line)
|
||||||
|
x.parent:remove(x.parent_index)
|
||||||
|
n_tag_sections = n_tag_sections + 1
|
||||||
|
x.parent:insert(n_tag_sections, x)
|
||||||
|
end
|
||||||
|
end, b)
|
||||||
|
|
||||||
|
-- b:insert(1, H.as_struct({ string.rep('=', 78) }, 'section'))
|
||||||
|
b:insert(H.as_struct({ '' }, 'section'))
|
||||||
|
end,
|
||||||
|
doc = function(d)
|
||||||
|
-- Render table of contents
|
||||||
|
H.apply_recursively(function(x)
|
||||||
|
if not (type(x) == 'table' and x.type == 'section' and x.info.id == '@toc') then return end
|
||||||
|
H.toc_insert(x)
|
||||||
|
end, d)
|
||||||
|
|
||||||
|
-- Insert modeline
|
||||||
|
d:insert(
|
||||||
|
H.as_struct(
|
||||||
|
{ H.as_struct({ H.as_struct({ ' vim:tw=78:ts=8:noet:ft=help:norl:' }, 'section') }, 'block') },
|
||||||
|
'file'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
sections = {
|
||||||
|
['@generic'] = function(s)
|
||||||
|
s:remove(1)
|
||||||
|
end,
|
||||||
|
['@field'] = function(s)
|
||||||
|
-- H.mark_optional(s)
|
||||||
|
if string.find(s[1], "^private ") then
|
||||||
|
s:remove(1)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
H.enclose_var_name(s)
|
||||||
|
H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
|
||||||
|
local wrapped = wrap(s[1], 78, "")
|
||||||
|
s:remove(1)
|
||||||
|
for i, line in ipairs(wrapped) do
|
||||||
|
s:insert(i, line)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
['@alias'] = function(s)
|
||||||
|
local name = s[1]:match('%s*(%S*)')
|
||||||
|
local alias = s[1]:match('%s(.*)$')
|
||||||
|
s[1] = ("`%s` → `%s`"):format(name, alias)
|
||||||
|
H.add_section_heading(s, 'Alias')
|
||||||
|
s:insert(1, H.as_struct({ ("*%s*"):format(name) }, "section", { id = "@tag" }))
|
||||||
|
end,
|
||||||
|
['@param'] = function(s)
|
||||||
|
H.enclose_var_name(s)
|
||||||
|
H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
|
||||||
|
local wrapped = wrap(s[1], 78, "")
|
||||||
|
s:remove(1)
|
||||||
|
for i, line in ipairs(wrapped) do
|
||||||
|
s:insert(i, line)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
['@return'] = function(s)
|
||||||
|
H.enclose_type(s, '`%(%1%)`', 1)
|
||||||
|
H.add_section_heading(s, 'Return')
|
||||||
|
end,
|
||||||
|
['@nodoc'] = function(s) s.parent:clear_lines() end,
|
||||||
|
['@class'] = function(s)
|
||||||
|
H.enclose_var_name(s)
|
||||||
|
-- Add heading
|
||||||
|
local line = s[1]
|
||||||
|
s:remove(1)
|
||||||
|
local class_name = string.match(line, "%{(.*)%}")
|
||||||
|
local inherits = string.match(line, ": (.*)")
|
||||||
|
if inherits then
|
||||||
|
s:insert(1, ("Inherits: `%s`"):format(inherits))
|
||||||
|
s:insert(2, "")
|
||||||
|
end
|
||||||
|
s:insert(1, H.as_struct({ ("*%s*"):format(class_name) }, "section", { id = "@tag" }))
|
||||||
|
end,
|
||||||
|
['@signature'] = function(s)
|
||||||
|
s[1] = H.format_signature(s[1])
|
||||||
|
if s[1] ~= "" then
|
||||||
|
table.insert(s, "")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
file = function(f)
|
||||||
|
if not f:has_lines() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if f.info.path ~= "./lua/" .. module .. "/init.lua" then
|
||||||
|
f:insert(1, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
|
||||||
|
f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
|
||||||
|
else
|
||||||
|
f:insert(
|
||||||
|
1,
|
||||||
|
H.as_struct(
|
||||||
|
{
|
||||||
|
H.as_struct(
|
||||||
|
{ header },
|
||||||
|
"section"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"block"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
f:insert(2, H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
|
||||||
|
f:insert(3, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
|
||||||
|
f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
minidoc.setup({})
|
||||||
|
minidoc.generate(
|
||||||
|
{
|
||||||
|
"./lua/nio/init.lua",
|
||||||
|
"./lua/nio/control.lua",
|
||||||
|
"./lua/nio/lsp.lua",
|
||||||
|
"./lua/nio/uv.lua",
|
||||||
|
"./lua/nio/ui.lua",
|
||||||
|
"./lua/nio/tests.lua",
|
||||||
|
|
||||||
|
},
|
||||||
|
"doc/nio.txt",
|
||||||
|
create_config("nio", "*nvim-nio.txt* A library for asynchronous IO in Neovim")
|
||||||
|
|
||||||
|
)
|
535
scripts/generate_lsp_types.lua
Normal file
535
scripts/generate_lsp_types.lua
Normal file
|
@ -0,0 +1,535 @@
|
||||||
|
---@class Model
|
||||||
|
---@field requests Request[]
|
||||||
|
---@field notifications Notification[]
|
||||||
|
---@field enumerations Enumeration
|
||||||
|
---@field typeAliases TypeAlias[]
|
||||||
|
---@field structures Structure[]
|
||||||
|
|
||||||
|
---@alias BaseTypes "URI" | "DocumentUri" | "integer" | "uinteger" | "decimal" | "RegExp" | "string" | "boolean" | "null"
|
||||||
|
|
||||||
|
---@class BooleanLiteralType
|
||||||
|
---@field kind "boolean"
|
||||||
|
---@field value boolean
|
||||||
|
|
||||||
|
---@class EnumerationEntry
|
||||||
|
---@field documentation? string An optional documentation.
|
||||||
|
---@field name string The name of the enum item.
|
||||||
|
---@field proposed? boolean Whether this is a proposed enumeration entry. If omitted, the enumeration entry is final.
|
||||||
|
---@field since? string Since when (release number) this enumeration entry is available. Is undefined if not known.
|
||||||
|
---@field value string | float The value.
|
||||||
|
|
||||||
|
---@alias Name "string" | "integer" | "uinteger"
|
||||||
|
|
||||||
|
---@class EnumerationType
|
||||||
|
---@field kind "enumeration"
|
||||||
|
---@field name Name
|
||||||
|
|
||||||
|
---@class IntegerLiteralType
|
||||||
|
---@field kind "integer" Represents an integer literal type (e.g. `kind: 1`).
|
||||||
|
---@field value float
|
||||||
|
|
||||||
|
---@alias Name1 "URI" | "DocumentUri" | "string" | "integer"
|
||||||
|
|
||||||
|
---@class MapKeyTypeItem
|
||||||
|
---@field kind TypeKind
|
||||||
|
---@field name Name1
|
||||||
|
|
||||||
|
---@alias MessageDirection "clientToServer" | "serverToClient" | "both"
|
||||||
|
|
||||||
|
---@class MetaData
|
||||||
|
---@field version string The protocol version.
|
||||||
|
|
||||||
|
---@class ReferenceType
|
||||||
|
---@field kind "reference"
|
||||||
|
---@field name string
|
||||||
|
|
||||||
|
---@class StringLiteralType
|
||||||
|
---@field kind "stringLiteral"
|
||||||
|
---@field value string
|
||||||
|
|
||||||
|
---@alias TypeKind "base" | "reference" | "array" | "map" | "and" | "or" | "tuple" | "literal" | "stringLiteral" | "integerLiteral" | "booleanLiteral"
|
||||||
|
|
||||||
|
---@class BaseType
|
||||||
|
---@field kind "base"
|
||||||
|
---@field name BaseTypes
|
||||||
|
|
||||||
|
---@class Enumeration
|
||||||
|
---@field documentation? string An optional documentation.
|
||||||
|
---@field name string The name of the enumeration.
|
||||||
|
---@field proposed? boolean Whether this is a proposed enumeration. If omitted, the enumeration is final.
|
||||||
|
---@field since? string Since when (release number) this enumeration is available. Is undefined if not known.
|
||||||
|
---@field supportsCustomValues? boolean Whether the enumeration supports custom values (e.g. values which are not part of the set defined in `values`). If omitted no custom values are supported.
|
||||||
|
---@field type EnumerationType The type of the elements.
|
||||||
|
---@field values EnumerationEntry[] The enum values.
|
||||||
|
|
||||||
|
---- Represents a type that can be used as a key in a map type. If a reference type is used then the type must either resolve to a `string` or `integer` type. (e.g. `type ChangeAnnotationIdentifier === string`).
|
||||||
|
---@alias MapKeyType MapKeyTypeItem | ReferenceType
|
||||||
|
|
||||||
|
---@class AndType
|
||||||
|
---@field items Type[]
|
||||||
|
---@field kind "and"
|
||||||
|
|
||||||
|
---@class ArrayType
|
||||||
|
---@field element Type
|
||||||
|
---@field kind "array"
|
||||||
|
|
||||||
|
---@class MapType
|
||||||
|
---@field key MapKeyType
|
||||||
|
---@field kind "map"
|
||||||
|
---@field value Type
|
||||||
|
|
||||||
|
---@class MetaModel
|
||||||
|
---@field enumerations Enumeration[] The enumerations.
|
||||||
|
---@field metaData MetaData Additional meta data.
|
||||||
|
---@field notifications Notification[] The notifications.
|
||||||
|
---@field requests Request[] The requests.
|
||||||
|
---@field structures Structure[] The structures.
|
||||||
|
---@field typeAliases TypeAlias[] The type aliases.
|
||||||
|
|
||||||
|
---@class Notification
|
||||||
|
---@field documentation? string An optional documentation;
|
||||||
|
---@field messageDirection MessageDirection The direction in which this notification is sent in the protocol.
|
||||||
|
---@field method string The request's method name.
|
||||||
|
---@field params? Type | Type[] The parameter type(s) if any.
|
||||||
|
---@field proposed? boolean Whether this is a proposed notification. If omitted the notification is final.
|
||||||
|
---@field registrationMethod? string Optional a dynamic registration method if it different from the request's method.
|
||||||
|
---@field registrationOptions? Type Optional registration options if the notification supports dynamic registration.
|
||||||
|
---@field since? string Since when (release number) this notification is available. Is undefined if not known.
|
||||||
|
|
||||||
|
---@class OrType
|
||||||
|
---@field items Type[]
|
||||||
|
---@field kind "or"
|
||||||
|
|
||||||
|
---@class Property
|
||||||
|
---@field documentation? string An optional documentation.
|
||||||
|
---@field name string The property name;
|
||||||
|
---@field optional? boolean Whether the property is optional. If omitted, the property is mandatory.
|
||||||
|
---@field proposed? boolean Whether this is a proposed property. If omitted, the structure is final.
|
||||||
|
---@field since? string Since when (release number) this property is available. Is undefined if not known.
|
||||||
|
---@field type Type The type of the property
|
||||||
|
|
||||||
|
---@class Request
|
||||||
|
---@field documentation? string An optional documentation;
|
||||||
|
---@field errorData? Type An optional error data type.
|
||||||
|
---@field messageDirection MessageDirection The direction in which this request is sent in the protocol.
|
||||||
|
---@field method string The request's method name.
|
||||||
|
---@field params? Type | Type[] The parameter type(s) if any.
|
||||||
|
---@field partialResult? Type Optional partial result type if the request supports partial result reporting.
|
||||||
|
---@field proposed? boolean Whether this is a proposed feature. If omitted the feature is final.
|
||||||
|
---@field registrationMethod? string Optional a dynamic registration method if it different from the request's method.
|
||||||
|
---@field registrationOptions? Type Optional registration options if the request supports dynamic registration.
|
||||||
|
---@field result Type The result type.
|
||||||
|
---@field since? string Since when (release number) this request is available. Is undefined if not known.
|
||||||
|
|
||||||
|
---@class Structure
|
||||||
|
---@field documentation? string An optional documentation;
|
||||||
|
---@field extends? Type[] Structures extended from. This structures form a polymorphic type hierarchy.
|
||||||
|
---@field mixins? Type[] Structures to mix in. The properties of these structures are `copied` into this structure. Mixins don't form a polymorphic type hierarchy in LSP.
|
||||||
|
---@field name string The name of the structure.
|
||||||
|
---@field properties Property[] The properties.
|
||||||
|
---@field proposed? boolean Whether this is a proposed structure. If omitted, the structure is final.
|
||||||
|
---@field since? string Since when (release number) this structure is available. Is undefined if not known.
|
||||||
|
|
||||||
|
---@class StructureLiteral
|
||||||
|
---@field documentation? string An optional documentation.
|
||||||
|
---@field properties Property[] The properties.
|
||||||
|
---@field proposed? boolean Whether this is a proposed structure. If omitted, the structure is final.
|
||||||
|
---@field since? string Since when (release number) this structure is available. Is undefined if not known.
|
||||||
|
|
||||||
|
---@class StructureLiteralType
|
||||||
|
---@field kind "literal"
|
||||||
|
---@field value StructureLiteral
|
||||||
|
|
||||||
|
---@class TupleType
|
||||||
|
---@field items Type[]
|
||||||
|
---@field kind "tuple"
|
||||||
|
|
||||||
|
---@alias Type BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType
|
||||||
|
|
||||||
|
---@class TypeAlias
|
||||||
|
---@field documentation? string An optional documentation.
|
||||||
|
---@field name string The name of the type alias.
|
||||||
|
---@field proposed? boolean Whether this is a proposed type alias. If omitted, the type alias is final.
|
||||||
|
---@field since? string Since when (release number) this structure is available. Is undefined if not known.
|
||||||
|
---@field type Type The aliased type.
|
||||||
|
|
||||||
|
---@class Generator
|
||||||
|
---@field known_objs table<string, Structure | TypeAlias>
|
||||||
|
---@field known_literals table<StructureLiteral, Structure>
|
||||||
|
---@field model Model
|
||||||
|
local Generator = {}
|
||||||
|
Generator.__index = Generator
|
||||||
|
|
||||||
|
function Generator.new(model)
|
||||||
|
local self = setmetatable({}, Generator)
|
||||||
|
self.model = model
|
||||||
|
self.known_objs = {}
|
||||||
|
self.known_literals = {}
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param obj Structure | TypeAlias | StructureLiteral
|
||||||
|
---@return Structure | TypeAlias
|
||||||
|
function Generator:register(obj)
|
||||||
|
if not obj.name then
|
||||||
|
if not self.known_literals[obj] then
|
||||||
|
self.known_literals[obj] = {
|
||||||
|
name = ("Structure%s"):format(#vim.tbl_keys(self.known_literals)),
|
||||||
|
documentation = obj.documentation,
|
||||||
|
properties = obj.properties,
|
||||||
|
proposed = obj.proposed,
|
||||||
|
since = obj.since,
|
||||||
|
extends = {},
|
||||||
|
mixins = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
obj = self.known_literals[obj]
|
||||||
|
end
|
||||||
|
print(("Registering %s"):format(obj.name))
|
||||||
|
if not self.known_objs[obj.name] then
|
||||||
|
self.known_objs[obj.name] = obj
|
||||||
|
end
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@return string
|
||||||
|
function Generator:convert_method_name(name)
|
||||||
|
local new_name = name:gsub("/", "_"):gsub("%$", "_")
|
||||||
|
return new_name
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function Generator:type_prefix()
|
||||||
|
return "nio.lsp.types"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param orig_name string
|
||||||
|
---@return string
|
||||||
|
function Generator:structure_name(orig_name)
|
||||||
|
return self:type_prefix() .. "." .. orig_name
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param items ReferenceType[]
|
||||||
|
---@return Structure
|
||||||
|
function Generator:and_type(items)
|
||||||
|
local names = vim.tbl_map(function(item)
|
||||||
|
return item.name
|
||||||
|
end, items)
|
||||||
|
local sub_structure = {
|
||||||
|
name = table.concat(names, "And"),
|
||||||
|
documentation = "",
|
||||||
|
extends = items,
|
||||||
|
properties = {},
|
||||||
|
mixins = {},
|
||||||
|
proposed = nil,
|
||||||
|
since = nil,
|
||||||
|
}
|
||||||
|
return sub_structure
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name Name | Name1
|
||||||
|
---@return string
|
||||||
|
function Generator:key_name_type(name)
|
||||||
|
if name == "URI" then
|
||||||
|
return self:type_prefix() .. ".URI"
|
||||||
|
elseif name == "DocumentUri" then
|
||||||
|
return self:type_prefix() .. ".DocumentUri"
|
||||||
|
else
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param type_ Type | MapKeyType)
|
||||||
|
---@return string
|
||||||
|
function Generator:type_name(type_)
|
||||||
|
if type_.kind == "base" then
|
||||||
|
local name = type_.name
|
||||||
|
if name == "integer" or name == "uinteger" then
|
||||||
|
return "integer"
|
||||||
|
elseif name == "decimal" then
|
||||||
|
return "number"
|
||||||
|
elseif name == "string" then
|
||||||
|
return "string"
|
||||||
|
elseif name == "boolean" then
|
||||||
|
return "boolean"
|
||||||
|
elseif name == "null" then
|
||||||
|
return "nil"
|
||||||
|
else
|
||||||
|
return self:key_name_type(name)
|
||||||
|
end
|
||||||
|
elseif type_.kind == "reference" then
|
||||||
|
local name = type_.name
|
||||||
|
return self:structure_name(name)
|
||||||
|
elseif type_.kind == "array" then
|
||||||
|
local element = type_.element
|
||||||
|
return self:type_name(element) .. "[]"
|
||||||
|
elseif type_.kind == "map" then
|
||||||
|
local key, value = type_.key, type_.value
|
||||||
|
if key.kind == "reference" then
|
||||||
|
return ("table<%s, %s>"):format(self:type_name(key), self:type_name(value))
|
||||||
|
else
|
||||||
|
local name = key.name
|
||||||
|
return ("table<%s, %s>"):format(self:key_name_type(name), self:type_name(value))
|
||||||
|
end
|
||||||
|
elseif type_.kind == "and" then
|
||||||
|
local items = type_.items
|
||||||
|
local refs = {}
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
if item.kind == "reference" then
|
||||||
|
refs[#refs + 1] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #items > #refs then
|
||||||
|
print(("Discarding non-reference/literal types from AndType"):format())
|
||||||
|
end
|
||||||
|
local struc = self:and_type(refs)
|
||||||
|
self:register(struc)
|
||||||
|
return self:structure_name(struc.name)
|
||||||
|
elseif type_.kind == "or" then
|
||||||
|
local items = type_.items
|
||||||
|
local names = vim.tbl_map(function(item)
|
||||||
|
return self:type_name(item)
|
||||||
|
end, items)
|
||||||
|
|
||||||
|
return table.concat(names, "|")
|
||||||
|
elseif type_.kind == "tuple" then
|
||||||
|
local items = type_.items
|
||||||
|
local names = vim.tbl_map(function(item)
|
||||||
|
return self:type_name(item)
|
||||||
|
end, items)
|
||||||
|
|
||||||
|
return table.concat(names, ",")
|
||||||
|
elseif type_.kind == "literal" then
|
||||||
|
local value = type_.value
|
||||||
|
local struc = self:register(value)
|
||||||
|
return self:structure_name(struc.name)
|
||||||
|
elseif type_.kind == "stringLiteral" then
|
||||||
|
local value = type_.value
|
||||||
|
return ("'%s'"):format(value)
|
||||||
|
end
|
||||||
|
error("Unknown type " .. type_.kind)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param doc string
|
||||||
|
---@param multiline boolean
|
||||||
|
---@return string[]
|
||||||
|
function Generator:prepare_doc(doc, multiline)
|
||||||
|
local lines = vim.split(doc, "\n", { trimempty = false, plain = true })
|
||||||
|
if multiline then
|
||||||
|
return vim.tbl_map(function(line)
|
||||||
|
return #line and ("--- %s"):format(line) or "---"
|
||||||
|
end, lines)
|
||||||
|
end
|
||||||
|
return { table.concat(lines, " ") }
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param structure Structure
|
||||||
|
---@return string[]
|
||||||
|
function Generator:structure(structure)
|
||||||
|
local lines = { "" }
|
||||||
|
if structure.documentation then
|
||||||
|
vim.list_extend(lines, self:prepare_doc(structure.documentation, true))
|
||||||
|
end
|
||||||
|
|
||||||
|
lines[#lines + 1] = ("---@class %s"):format(self:structure_name(structure.name))
|
||||||
|
if structure.extends or structure.mixins then
|
||||||
|
local extends = vim.list_extend(vim.deepcopy(structure.extends or {}), structure.mixins or {})
|
||||||
|
local names = vim.tbl_map(function(type_)
|
||||||
|
return self:type_name(type_)
|
||||||
|
end, extends)
|
||||||
|
if #names > 0 then
|
||||||
|
lines[#lines] = lines[#lines] .. " : " .. table.concat(names, ",")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, prop in ipairs(structure.properties) do
|
||||||
|
local line = ("---@field %s%s %s"):format(
|
||||||
|
prop.name,
|
||||||
|
prop.optional and "?" or "",
|
||||||
|
self:type_name(prop.type)
|
||||||
|
)
|
||||||
|
if prop.documentation then
|
||||||
|
line = line .. " " .. self:prepare_doc(prop.documentation, false)[1]
|
||||||
|
end
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param type_alias TypeAlias
|
||||||
|
---@return string[]
|
||||||
|
function Generator:type_alias(type_alias)
|
||||||
|
self:register(type_alias)
|
||||||
|
return {
|
||||||
|
("---@alias %s.%s %s"):format(
|
||||||
|
self:type_prefix(),
|
||||||
|
type_alias.name,
|
||||||
|
self:type_name(type_alias.type)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param request Request
|
||||||
|
---@return string[]
|
||||||
|
function Generator:request(request)
|
||||||
|
local lines = {}
|
||||||
|
if request.documentation then
|
||||||
|
vim.list_extend(lines, self:prepare_doc(request.documentation, true))
|
||||||
|
end
|
||||||
|
|
||||||
|
lines[#lines + 1] = "---@async"
|
||||||
|
if request.params then
|
||||||
|
lines[#lines + 1] = ("---@param args %s Arguments to the request"):format(self:type_name(request.params))
|
||||||
|
end
|
||||||
|
lines[#lines + 1] = "---@param bufnr integer? Buffer number (0 for current buffer)"
|
||||||
|
lines[#lines + 1] = "---@param opts? nio.lsp.RequestOpts Options for the request handling"
|
||||||
|
lines[#lines + 1] = ("---@return %s.ResponseError|nil error The error object in case a request fails."):format(self:type_prefix())
|
||||||
|
if request.result then
|
||||||
|
lines[#lines + 1] = (("---@return %s"):format(self:type_name(request.result)))
|
||||||
|
if not vim.endswith(lines[#lines], "|nil") then
|
||||||
|
lines[#lines] = lines[#lines] .. "|nil"
|
||||||
|
end
|
||||||
|
lines[#lines] = lines[#lines] .. " result The result of the request"
|
||||||
|
end
|
||||||
|
|
||||||
|
lines[#lines + 1] = (
|
||||||
|
("function LSPRequestClient.%s(%sbufnr, opts) end"):format(
|
||||||
|
self:convert_method_name(request.method),
|
||||||
|
request.params and "args, " or ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lines[#lines + 1] = ""
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param notification Notification
|
||||||
|
---@return string[]
|
||||||
|
function Generator:notification(notification)
|
||||||
|
local lines = {}
|
||||||
|
if notification.documentation then
|
||||||
|
vim.list_extend(lines, self:prepare_doc(notification.documentation, true))
|
||||||
|
end
|
||||||
|
|
||||||
|
lines[#lines + 1] = "---@async"
|
||||||
|
if notification.params then
|
||||||
|
lines[#lines + 1] = (("---@param args %s"):format(self:type_name(notification.params)))
|
||||||
|
lines[#lines + 1] = (
|
||||||
|
("function LSPNotifyClient.%s(%s) end"):format(
|
||||||
|
self:convert_method_name(notification.method),
|
||||||
|
notification.params and "args" or ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lines[#lines + 1] = ""
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param enum Enumeration
|
||||||
|
---@return string[]
|
||||||
|
function Generator:enumeration(enum)
|
||||||
|
local lines = {}
|
||||||
|
if enum.documentation then
|
||||||
|
vim.list_extend(lines, self:prepare_doc(enum.documentation, true))
|
||||||
|
end
|
||||||
|
lines[#lines + 1] = (
|
||||||
|
("---@alias %s.%s %s"):format(
|
||||||
|
self:type_prefix(),
|
||||||
|
enum.name,
|
||||||
|
table.concat(
|
||||||
|
vim.tbl_map(function(val)
|
||||||
|
return vim.json.encode(val.value)
|
||||||
|
end, enum.values),
|
||||||
|
"|"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lines[#lines + 1] = ""
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
function Generator:generate()
|
||||||
|
local lines = {
|
||||||
|
("---Generated on %s"):format(os.date("!%Y-%m-%d-%H:%M:%S GMT")),
|
||||||
|
"",
|
||||||
|
"---@class nio.lsp.RequestClient",
|
||||||
|
"local LSPRequestClient = {}",
|
||||||
|
"---@class nio.lsp.RequestOpts",
|
||||||
|
"---@field timeout integer Timeout of request in milliseconds",
|
||||||
|
"---@class nio.lsp.types.ResponseError",
|
||||||
|
"---@field code number A number indicating the error type that occurred.",
|
||||||
|
"---@field message string A string providing a short description of the error.",
|
||||||
|
"---@field data any A Primitive or Structured value that contains additional information about the error. Can be omitted.",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
local strucs = {}
|
||||||
|
vim.list_extend(strucs, self.model.structures)
|
||||||
|
vim.list_extend(strucs, self.model.typeAliases)
|
||||||
|
vim.list_extend(strucs, self.model.enumerations)
|
||||||
|
for _, obj in ipairs(strucs) do
|
||||||
|
self:register(obj)
|
||||||
|
end
|
||||||
|
print("Generating requests")
|
||||||
|
for _, request in ipairs(self.model.requests) do
|
||||||
|
if request.messageDirection == "clientToServer" or request.messageDirection == "both" then
|
||||||
|
vim.list_extend(lines, self:request(request))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.list_extend(lines, {
|
||||||
|
"---@class nio.lsp.NotifyClient",
|
||||||
|
"local LSPNotifyClient = {}",
|
||||||
|
"",
|
||||||
|
})
|
||||||
|
print("Generating notifications")
|
||||||
|
for _, notification in ipairs(self.model.notifications) do
|
||||||
|
if notification.messageDirection == "clientToServer" or notification.messageDirection == "both"
|
||||||
|
then
|
||||||
|
vim.list_extend(lines, self:notification(notification))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.list_extend(lines, {
|
||||||
|
("---@alias %s string"):format(self:key_name_type("URI")),
|
||||||
|
("---@alias %s string"):format(self:key_name_type("DocumentUri")),
|
||||||
|
})
|
||||||
|
|
||||||
|
local length = function()
|
||||||
|
return #vim.tbl_keys(self.known_objs)
|
||||||
|
end
|
||||||
|
local last_length = 0
|
||||||
|
|
||||||
|
print("Discovering types")
|
||||||
|
while length() > last_length do
|
||||||
|
for _, obj in pairs(self.known_objs) do
|
||||||
|
if obj.properties then
|
||||||
|
self:structure(obj)
|
||||||
|
elseif obj.values then
|
||||||
|
self:enumeration(obj)
|
||||||
|
else
|
||||||
|
self:type_alias(obj)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_length = length()
|
||||||
|
end
|
||||||
|
|
||||||
|
print("Generating structures")
|
||||||
|
for _, obj in pairs(self.known_objs) do
|
||||||
|
if obj.properties then
|
||||||
|
vim.list_extend(lines, self:structure(obj))
|
||||||
|
elseif obj.values then
|
||||||
|
vim.list_extend(lines, self:enumeration(obj))
|
||||||
|
else
|
||||||
|
vim.list_extend(lines, self:type_alias(obj))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
print(("Generated %d lines\n"):format(#lines))
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = assert(io.open("lsp.json"))
|
||||||
|
local model = vim.json.decode(file:read("*a"))
|
||||||
|
file:close()
|
||||||
|
local lines = Generator.new(model):generate()
|
||||||
|
|
||||||
|
local out = assert(io.open("lua/nio/lsp-types.lua", "w"))
|
||||||
|
out:write(table.concat(lines, "\n"))
|
||||||
|
out:close()
|
||||||
|
vim.cmd("exit")
|
3
scripts/style
Executable file
3
scripts/style
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
stylua lua tests
|
20
scripts/test
Executable file
20
scripts/test
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
tempfile=".test_output.tmp"
|
||||||
|
|
||||||
|
if [[ -n $1 ]]; then
|
||||||
|
nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedFile $1" | tee "${tempfile}"
|
||||||
|
else
|
||||||
|
nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.vim'}" | tee "${tempfile}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plenary doesn't emit exit code 1 when tests have errors during setup
|
||||||
|
errors=$(sed 's/\x1b\[[0-9;]*m//g' "${tempfile}" | awk '/(Errors|Failed) :/ {print $3}' | grep -v '0')
|
||||||
|
|
||||||
|
rm "${tempfile}"
|
||||||
|
|
||||||
|
if [[ -n $errors ]]; then
|
||||||
|
echo "Tests failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
5
stylua.toml
Normal file
5
stylua.toml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
column_width = 100
|
||||||
|
line_endings = "Unix"
|
||||||
|
indent_type = "Spaces"
|
||||||
|
indent_width = 2
|
||||||
|
quote_style = "AutoPreferDouble"
|
16
test.lua
Normal file
16
test.lua
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
local nio = require("nio")
|
||||||
|
|
||||||
|
nio.run(function()
|
||||||
|
|
||||||
|
local client = nio.lsp.get_clients({ name = "lua_ls" })[1]
|
||||||
|
|
||||||
|
local err, response = client.request.textDocument_semanticTokens_full({
|
||||||
|
textDocument = { uri = vim.uri_from_bufnr(0) },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert(not err, err)
|
||||||
|
|
||||||
|
for _, i in pairs(response and response.data or {}) do
|
||||||
|
print(i)
|
||||||
|
end
|
||||||
|
end)
|
145
tests/control_spec.lua
Normal file
145
tests/control_spec.lua
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
local nio = require("nio")
|
||||||
|
local a = nio.tests
|
||||||
|
|
||||||
|
describe("event", function()
|
||||||
|
a.it("notifies listeners", function()
|
||||||
|
local event = nio.control.event()
|
||||||
|
local notified = 0
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
event.wait()
|
||||||
|
notified = notified + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
event.set()
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals(10, notified)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("notifies listeners when already set", function()
|
||||||
|
local event = nio.control.event()
|
||||||
|
local notified = 0
|
||||||
|
event.set()
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
event.wait()
|
||||||
|
notified = notified + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals(10, notified)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("future", function()
|
||||||
|
a.it("provides listeners result", function()
|
||||||
|
local future = nio.control.future()
|
||||||
|
local notified = 0
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
local val = future.wait()
|
||||||
|
notified = notified + val
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
future.set(1)
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals(10, notified)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("notifies listeners when already set", function()
|
||||||
|
local future = nio.control.future()
|
||||||
|
local notified = 0
|
||||||
|
future.set(1)
|
||||||
|
for _ = 1, 10 do
|
||||||
|
nio.run(function()
|
||||||
|
notified = notified + future.wait()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals(10, notified)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("raises error for listeners", function()
|
||||||
|
local future = nio.control.future()
|
||||||
|
local notified = 0
|
||||||
|
future.set_error("test")
|
||||||
|
local success, err = pcall(future.wait)
|
||||||
|
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.False(success)
|
||||||
|
assert.True(vim.endswith(err, "test"))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
describe("queue", function()
|
||||||
|
a.it("adds and removes items", function()
|
||||||
|
local queue = nio.control.queue()
|
||||||
|
queue.put(1)
|
||||||
|
queue.put(2)
|
||||||
|
|
||||||
|
assert.same(queue.size(), 2)
|
||||||
|
assert.same(1, queue.get())
|
||||||
|
assert.same(2, queue.get())
|
||||||
|
assert.same(queue.size(), 0)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("get blocks while empty", function()
|
||||||
|
local queue = nio.control.queue()
|
||||||
|
nio.run(function()
|
||||||
|
nio.sleep(10)
|
||||||
|
queue.put(1)
|
||||||
|
end)
|
||||||
|
assert.same(1, queue.get())
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("put blocks while full", function()
|
||||||
|
local queue = nio.control.queue(1)
|
||||||
|
nio.run(function()
|
||||||
|
nio.sleep(10)
|
||||||
|
queue.get()
|
||||||
|
end)
|
||||||
|
queue.put(1)
|
||||||
|
queue.put(2)
|
||||||
|
assert.same(2, queue.get())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("get_nowait errors when empty", function()
|
||||||
|
local queue = nio.control.queue()
|
||||||
|
assert.error(queue.get_nowait)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("put_nowait errors while full", function()
|
||||||
|
local queue = nio.control.queue(1)
|
||||||
|
queue.put_nowait(1)
|
||||||
|
assert.error(function()
|
||||||
|
queue.put_nowait(2)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("semaphore", function()
|
||||||
|
a.it("only allows permitted number of concurrent accesses", function()
|
||||||
|
local concurrent = 0
|
||||||
|
local max_concurrent = 0
|
||||||
|
local allowed = 3
|
||||||
|
local semaphore = nio.control.semaphore(allowed)
|
||||||
|
local worker = function()
|
||||||
|
semaphore.with(function()
|
||||||
|
concurrent = concurrent + 1
|
||||||
|
max_concurrent = math.max(max_concurrent, concurrent)
|
||||||
|
nio.sleep(10)
|
||||||
|
concurrent = concurrent - 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
local workers = {}
|
||||||
|
for _ = 1, 10 do
|
||||||
|
table.insert(workers, worker)
|
||||||
|
end
|
||||||
|
nio.gather(workers)
|
||||||
|
|
||||||
|
assert.same(max_concurrent, allowed)
|
||||||
|
end)
|
||||||
|
end)
|
1
tests/init.vim
Normal file
1
tests/init.vim
Normal file
|
@ -0,0 +1 @@
|
||||||
|
source tests/minimal_init.lua
|
118
tests/init_spec.lua
Normal file
118
tests/init_spec.lua
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
local nio = require("nio")
|
||||||
|
local a = nio.tests
|
||||||
|
|
||||||
|
describe("async helpers", function()
|
||||||
|
a.it("sleep", function()
|
||||||
|
local start = vim.loop.now()
|
||||||
|
nio.sleep(10)
|
||||||
|
local end_ = vim.loop.now()
|
||||||
|
assert.True(end_ - start >= 10)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("wrap returns values provided to callback", function()
|
||||||
|
local result
|
||||||
|
local wrapped = nio.wrap(function(_, _, cb)
|
||||||
|
cb(1, 2)
|
||||||
|
end, 3)
|
||||||
|
nio.run(wrapped, function(_, ...)
|
||||||
|
result = { ... }
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert.same({ 1, 2 }, result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("gather returns results", function()
|
||||||
|
local worker = function(i)
|
||||||
|
return function()
|
||||||
|
nio.sleep(100 - (i * 10))
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local workers = {}
|
||||||
|
for i = 1, 10 do
|
||||||
|
table.insert(workers, worker(i))
|
||||||
|
end
|
||||||
|
|
||||||
|
local results = nio.gather(workers)
|
||||||
|
assert.same({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, results)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("gather raises errors", function()
|
||||||
|
local worker = function(i)
|
||||||
|
return function()
|
||||||
|
if i == 4 then
|
||||||
|
error("error")
|
||||||
|
end
|
||||||
|
nio.sleep(100 - (i * 10))
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local workers = {}
|
||||||
|
for i = 1, 10 do
|
||||||
|
table.insert(workers, worker(i))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert.error(function()
|
||||||
|
nio.gather(workers)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("first returns first result", function()
|
||||||
|
local worker = function(i)
|
||||||
|
return function()
|
||||||
|
nio.sleep(100 - (i * 10))
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local workers = {}
|
||||||
|
for i = 1, 10 do
|
||||||
|
table.insert(workers, worker(i))
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = nio.first(workers)
|
||||||
|
assert.same(10, result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("first cancels pending tasks", function()
|
||||||
|
local worker = function(i)
|
||||||
|
return function()
|
||||||
|
nio.sleep(100 - (i * 10))
|
||||||
|
if i ~= 10 then
|
||||||
|
error("error")
|
||||||
|
end
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local workers = {}
|
||||||
|
for i = 1, 10 do
|
||||||
|
table.insert(workers, worker(i))
|
||||||
|
end
|
||||||
|
|
||||||
|
nio.first(workers)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("first raises errors", function()
|
||||||
|
local worker = function(i)
|
||||||
|
return function()
|
||||||
|
if i == 4 then
|
||||||
|
error("error")
|
||||||
|
end
|
||||||
|
nio.sleep(100 - (i * 10))
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local workers = {}
|
||||||
|
for i = 1, 10 do
|
||||||
|
table.insert(workers, worker(i))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert.error(function()
|
||||||
|
nio.first(workers)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
96
tests/lsp_spec.lua
Normal file
96
tests/lsp_spec.lua
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
local nio = require("nio")
|
||||||
|
local a = nio.tests
|
||||||
|
|
||||||
|
describe("lsp client", function()
|
||||||
|
a.it("sends request and returns result", function()
|
||||||
|
local expected_result = { "test" }
|
||||||
|
local expected_params = { a = "b" }
|
||||||
|
vim.lsp.get_client_by_id = function(id)
|
||||||
|
return {
|
||||||
|
request = function(method, params, callback, bufnr)
|
||||||
|
assert.equals("textDocument/diagnostic", method)
|
||||||
|
assert.equals(0, bufnr)
|
||||||
|
assert.same(params, params)
|
||||||
|
callback(nil, expected_result)
|
||||||
|
return true, 1
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local client = nio.lsp.client(1)
|
||||||
|
|
||||||
|
local err, result =
|
||||||
|
client.request.textDocument_diagnostic(expected_params, 0, { timeout = 1000 })
|
||||||
|
assert.same(expected_result, result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("returns error for request", function()
|
||||||
|
local params = { a = "b" }
|
||||||
|
vim.lsp.get_client_by_id = function(id)
|
||||||
|
return {
|
||||||
|
request = function(method, params, callback, bufnr)
|
||||||
|
callback({ message = "error" }, nil)
|
||||||
|
return true, 1
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local client = nio.lsp.client(1)
|
||||||
|
|
||||||
|
local err, result = client.request.textDocument_diagnostic(0, params)
|
||||||
|
assert.same(err.message, "error")
|
||||||
|
assert.Nil(result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("raises error on timeout", function()
|
||||||
|
vim.lsp.get_client_by_id = function(id)
|
||||||
|
return {
|
||||||
|
request = function(method, params, callback, bufnr)
|
||||||
|
return true, 1
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local client = nio.lsp.client(1)
|
||||||
|
|
||||||
|
local err, result = client.request.textDocument_diagnostic({}, 0, { timeout = 10 })
|
||||||
|
assert.same(err.message, "Request timed out")
|
||||||
|
assert.Nil(result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("cancels request on timeout", function()
|
||||||
|
local cancel_received = false
|
||||||
|
vim.lsp.get_client_by_id = function(id)
|
||||||
|
return {
|
||||||
|
request = function(method, params, callback, bufnr)
|
||||||
|
if method == "$/cancelRequest" then
|
||||||
|
cancel_received = true
|
||||||
|
end
|
||||||
|
return true, 1
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local client = nio.lsp.client(1)
|
||||||
|
|
||||||
|
client.request.textDocument_diagnostic({}, 0, { timeout = 10 })
|
||||||
|
assert.True(cancel_received)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("raises errors on client shutdown", function()
|
||||||
|
vim.lsp.get_client_by_id = function(id)
|
||||||
|
return {
|
||||||
|
id = id,
|
||||||
|
request = function(method, params, callback, bufnr)
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local client = nio.lsp.client(1)
|
||||||
|
|
||||||
|
local success, err = pcall(client.request.textDocument_diagnostic, {}, 0, { timeout = 10 })
|
||||||
|
assert.False(success)
|
||||||
|
assert.Not.Nil(string.find(err, "Client 1 has shut down"))
|
||||||
|
end)
|
||||||
|
end)
|
9
tests/minimal_init.lua
Normal file
9
tests/minimal_init.lua
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
local lazypath = vim.fn.stdpath("data") .. "/lazy"
|
||||||
|
vim.notify = print
|
||||||
|
vim.opt.rtp:append(".")
|
||||||
|
vim.opt.rtp:append(lazypath .. "/plenary.nvim")
|
||||||
|
vim.cmd("runtime! plugin/plenary.vim")
|
||||||
|
vim.opt.swapfile = false
|
||||||
|
A = function(...)
|
||||||
|
print(vim.inspect(...))
|
||||||
|
end
|
120
tests/tasks_spec.lua
Normal file
120
tests/tasks_spec.lua
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
local tasks = require("nio.tasks")
|
||||||
|
local nio = require("nio")
|
||||||
|
local a = nio.tests
|
||||||
|
|
||||||
|
describe("task", function()
|
||||||
|
a.it("provides result in callback", function()
|
||||||
|
local result
|
||||||
|
tasks.run(function()
|
||||||
|
nio.sleep(5)
|
||||||
|
return "test"
|
||||||
|
end, function(_, result_)
|
||||||
|
result = result_
|
||||||
|
end)
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.equals("test", result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("cancels", function()
|
||||||
|
local err
|
||||||
|
local task = tasks.run(function()
|
||||||
|
nio.sleep(10)
|
||||||
|
return "test"
|
||||||
|
end, function(_, err_)
|
||||||
|
err = err_
|
||||||
|
end)
|
||||||
|
task.cancel()
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.True(vim.endswith(vim.split(err, "\n")[1], "Task was cancelled"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("cancels children", function()
|
||||||
|
local should_be_nil
|
||||||
|
local task = tasks.run(function()
|
||||||
|
tasks.run(function()
|
||||||
|
nio.sleep(10)
|
||||||
|
should_be_nil = "not nil"
|
||||||
|
end)
|
||||||
|
nio.sleep(10)
|
||||||
|
end)
|
||||||
|
task.cancel()
|
||||||
|
nio.sleep(20)
|
||||||
|
assert.Nil(should_be_nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("assigns parent task", function()
|
||||||
|
local current = tasks.current_task()
|
||||||
|
local task = tasks.run(function()
|
||||||
|
return "test"
|
||||||
|
end)
|
||||||
|
assert.Not.Nil(task.parent)
|
||||||
|
assert.equal(current, task.parent)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("assigns no parent task", function()
|
||||||
|
local task = tasks.run(function()
|
||||||
|
return "test"
|
||||||
|
end)
|
||||||
|
assert.Nil(task.parent)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("returns error in function", function()
|
||||||
|
local success, err
|
||||||
|
tasks.run(function()
|
||||||
|
error("test")
|
||||||
|
end, function(success_, err_)
|
||||||
|
success, err = success_, err_
|
||||||
|
end)
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.False(success)
|
||||||
|
assert.True(vim.endswith(vim.split(err, "\n")[2], "test"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("returns error when wrapped function errors", function()
|
||||||
|
local success, err
|
||||||
|
local bad_wrapped = tasks.wrap(function()
|
||||||
|
error("test")
|
||||||
|
end, 1)
|
||||||
|
tasks.run(bad_wrapped, function(success_, err_)
|
||||||
|
success, err = success_, err_
|
||||||
|
end)
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.False(success)
|
||||||
|
assert.True(vim.endswith(vim.split(err, "\n")[2], "test"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("pcall returns result", function()
|
||||||
|
local success, x, y = pcall(function()
|
||||||
|
return 1, 2
|
||||||
|
end)
|
||||||
|
assert.True(success)
|
||||||
|
assert.equals(1, x)
|
||||||
|
assert.equals(2, y)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("pcall returns error", function()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
error("test")
|
||||||
|
end)
|
||||||
|
assert.False(success)
|
||||||
|
assert.True(vim.endswith(vim.split(err, "\n")[1], "test"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("pcall returns error when wrapped function errors", function()
|
||||||
|
local success, err = pcall(tasks.wrap(function(...)
|
||||||
|
error("test")
|
||||||
|
end, 1))
|
||||||
|
|
||||||
|
nio.sleep(10)
|
||||||
|
assert.False(success)
|
||||||
|
assert.True(vim.endswith(vim.split(err, "\n")[1], "test"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("current task", function()
|
||||||
|
local current
|
||||||
|
local task = tasks.run(function()
|
||||||
|
current = tasks.current_task()
|
||||||
|
end)
|
||||||
|
assert.equal(task, current)
|
||||||
|
end)
|
||||||
|
end)
|
31
tests/uv_spec.lua
Normal file
31
tests/uv_spec.lua
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
local nio = require("nio")
|
||||||
|
local a = nio.tests
|
||||||
|
|
||||||
|
describe("file operations", function()
|
||||||
|
local path = vim.fn.tempname()
|
||||||
|
a.after_each(function()
|
||||||
|
os.remove(path)
|
||||||
|
end)
|
||||||
|
a.it("reads a file", function()
|
||||||
|
local f = assert(io.open(path, "w"))
|
||||||
|
f:write("test read")
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
local _, file = nio.uv.fs_open(path, "r", 438)
|
||||||
|
local _, data = nio.uv.fs_read(file, 1024, -1)
|
||||||
|
nio.uv.fs_close(file)
|
||||||
|
assert.equals("test read", data)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a.it("writes a file", function()
|
||||||
|
local _, file = nio.uv.fs_open(path, "w", 438)
|
||||||
|
nio.uv.fs_write(file, "test write")
|
||||||
|
nio.uv.fs_close(file)
|
||||||
|
|
||||||
|
local file = assert(io.open(path, "r"))
|
||||||
|
local data = file:read()
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
assert.equals("test write", data)
|
||||||
|
end)
|
||||||
|
end)
|
Loading…
Reference in a new issue