Rest Roadmap
This post serves as a preamble for the development of the rest.nvim
rewrite with which it’s
expected to achieve an extremely notable improvement in speed, reliability, extensibility and
stability of the plugin.
This is a spiritual successor of an old
.norg
document I had with specs. None of these changes are final, everything is subject to change.
Current state
First of all, we have to talk about the current state of the project. Let’s see what’s wrong, how can we address these issues with a rewrite, and what will happen to the current pull requests.
What is wrong with the current codebase?
rest.nvim
was my first plugin for Neovim — which has already been around for 3 years, how quickly time flies. Because of that, it’s not as good code as what I would currently write, for example. In the same way, the code has been evolving without many changes, which is why we may have a couple of limitations or complications when wanting to change something internally, such as the HTTP parsing system.- We are currently using a
cURL
wrapper created in theplenary.nvim
library. This wrapper worked well when I started working on rest.nvim, but currently it is too basic for the needs of the project and working on improving it is not worth that much of the effort. - The
HTTP/1.1
specification used in current codebase (RFC 2616
) by design decision has been obsolete for a long time and limits us. - The view of the results can be improved a lot, but this requires a bit of redesign.
How can we address these issues with a rewrite?
Given the previous point, I think we can get an idea of why a rewrite is needed, right? However, let’s briefly explain some solutions that we will expand on later.
- Having a tree-sitter parser that already provides us with the information from the HTTP document we can optimize the obtaining of the requests and pass them to cURL more quickly, instead of using the current method that requires doing the parsing ourselves. Additionally, this would help us clean up the code a bit and reduce the parsing and execution process.
- Using a library with native cURL bindings using the Lua library Lua-cURL we will be able to have a much complete and native integration with cURL. To install this library we will use Luarocks. This will be explained in more depth later.
- Change the
HTTP/1.1
semantic specifications used to more current ones. We could useRFC 7231
, which was published in 2014, or its successor,RFC 9112
, which was published a little over a year ago. - The Neovim ecosystem has grown a lot in recent times, breaking down some limitations such as image rendering in buffers, and rest.nvim must adapt to new changes that bring improvement.
What will happen to the current pull requests?
Most likely only Pull Requests with bug fixes will be accepted, and the current version of rest.nvim will enter a maintenance phase (also known as maintenance mode) while the new version is being worked on.
But why this? Most of the Pull Requests that are still open are new features, but they conflict with the current changes in the main branch and we cannot merge them and in some of them the authors have not given signs of life.
However, those considered necessary or useful for the new version will be added manually and the original author will be included in the commits as Co-authored-by so that they continue to have their merit in participating in the project.
v2.0 - Thunder Rest
Version 2.0 of rest.nvim
, which I have named Thunder Rest
, will not be completely ready until
approximately the first two months of 2024 at the latest. This ETA may seem long, and will
probably be reduced if no issues or time constraints are encountered.
The Thunder Rest
release will follow the full versioning of semver. This
means, first it will enter an alpha phase, then it will go to beta when it is more stable, going
through release candidates and finally reaching a final version which is completely stable and
usable.
Note: we will refer to v2.0 as
Thunder Rest
throughout the specifications.
Specifications
This part of the post will be a bit long, so grab some coffee and get ready to see everything that is planned for the next release!
Installation
Installing rest.nvim
is possible and preferably through Luarocks and rocks.nvim. This is because we
will be using some dependencies that are there, such as Lua-cURL. In addition, Luarocks makes our
work easier by having the ability to manage dependencies for us.
Installing rest.nvim
will also still be possible through Git
and plugin manager like lazy.nvim
, but you will have to install the dependencies manually!
Notes:
- If you are using
lazy.nvim
, you will not be able to userocks.nvim
to installrest.nvim
due to the hijacks thatlazy.nvim
makes to the Neovim runtimepath by design.- If for some reason you still use
packer.nvim
as your plugin manager, you can install the dependencies directly from youruse()
declaration.- Instructions for manually installing dependencies without using
rocks.nvim
will be available in the README.
Documentation first and everywhere
Documentation is the most essential part of the software, especially if it is shared with someone else, as this is the pillar of understanding it.
This is why Thunder Rest
will have extensive documentation inside and outside the code to ensure a
positive coverage of understanding of how the project works and why it works the way it works.
If you currently look at the v1.0
code you will see little documentation in some parts that should
have it, which can make it difficult to quickly understand the code. This is mostly due to the first
point in the section explaining what is wrong with the current codebase, and it is something that
must be changed. Therefore, Thunder Rest
will have the documentation as a first-class citizen.
Goodbye setup, thanks for everything!
This release will say goodbye to the way rest.nvim
was initialized, but why? There are a number of good reasons behind it, and all
of them are explained in more detail in this blog
post.
This post basically proposes to separate the setup
function into two parts, config
and load
respectively, in order to decouple the configuration from the plugin initialization, which is
something we should have always done when writing plugins.
To avoid repeating what the post expresses here, it is better to take a look at the relevant parts of the post and then return here. It is short, precise and well explained!
In this way, configuring and loading rest.nvim
would look like this:
-- Configure rest.nvim
--
-- It is probably unnecessary, since there will be sensitive defaults.
require('rest-nvim').config({
-- Maybe there is a better way to catalog this?
behavior = {
-- ...
encode_url = true,
highlight = {
enabled = true,
timeout = 150,
},
-- ...
},
keybinds = {
-- Hey, what is this? O.o
},
-- ...
})
-- Load rest.nvim
--
-- No parameters required here, this loads the user keybinds, set up autocommands and uses sane
-- defaults for configurations if the config function was not called at any time.
require('rest-nvim').load()
Keybindings
The way rest.nvim
keybindings will change completely, to something more familiar and that works
out-of-the-box. We will try to maintain the philosophy that keybindings do not get in your way or
cause conflicts.
The way they currently work is not obtrusive, however sometimes people haven’t figured out how to
use them because they work differently than a normal keybind. From :h using-<Plug>
:
Both <SID> and <Plug> are used to avoid that mappings of typed keys interfere
with mappings that are only to be used from other mappings. Note the
difference between using <SID> and <Plug>:
<Plug> is visible outside of the script. It is used for mappings which the
user might want to map a key sequence to. <Plug> is a special code
that a typed key will never produce.
To make it very unlikely that other plugins use the same sequence of
characters, use this structure: <Plug> scriptname mapname
In our example the scriptname is "Typecorr" and the mapname is "Add".
We add a semicolon as the terminator. This results in
'<Plug>TypecorrAdd;'. Only the first character of scriptname and
mapname is uppercase, so that we can see where mapname starts.
To improve the quality of life of the user, Thunder Rest
will create a configuration table called
keybinds
in which the user can define their keybindings in a practical and convenient way. This
would be an example of how keybinds would be created now:
-- The values used here will be the default ones.
require('rest-nvim').config({
-- See `:h vim.keymap.set()`
keybinds = {
-- Keys | Action | Description
"<localleader>r", "<Plug>(RestRun)", "Run request under the cursor",
"<localleader>r", "<Plug>(RestLast)", "Re-run the last executed request",
"<localleader>r", "<Plug>(RestPreview)", "Preview the cURL command for the request under the cursor",
}
})
Notes:
- Please, if you don’t know what localleader is or how to use it, take a look at the documentation at
:h <LocalLeader>
- By default,
rest.nvim
will set<localleader>
to,
ifvim.g.maplocalleader
has not been defined.keybinds
table will automatically setrest.nvim
keybinds only for.http
files and will also setnoremap
.
Configuration
The configuration options will most likely remain the same, although some values may be adjusted to make them more sane or opinionated.
An example of this would be the recently added stay_in_current_window_after_split
option in
#257. This option causes the focus of the buffer
not to be changed to the HTTP request results buffer and although it’s very useful, it defaults to
false
so the default functionality is not changed. Since it’s considered a useful option, it will
be enabled by default, as part of the necessary “breaking changes” to the Thunder Rest
release.
Rest command
Thunder Rest
will expose a Neovim command called :Rest
. This command is a simple yet powerful
wrapper around the HTTP client that rest.nvim
is currently using under the hood (e.g. cURL
or HURL
).
This command works with subcommands — aka actions. Some of them work with additional and optional
flags like *nix
terminal utilities (e.g. wc -l
). You can complete these actions by pressing
<TAB>
and these actions are the following:
run
--last
- Re-run last executed request.--cursor
- Run the request under the cursor.--document
- Run all requests found in the current HTTP document.
last
(alias torun --last
to retain backwards compatibility with the old keybinds layout)preview
Rest functions
As :Rest
command is a wrapper around the current HTTP client, it does require some functions to be
exposed by ech third-party client init.lua
module (which we are going to explain later). This is a
public API, and can be used in place of the <Plug>
mappings if you want some more extensibility.
exec
Execute or preview
one or several HTTP requests depending on given scope
and return request(s)
results in a table that will be used to render results in a buffer.
Arguments
scope: string
- Defines the request execution scope. Can be:
last
,cursor
ordocument
.
- Defines the request execution scope. Can be:
preview: boolean
- Whether execute the request or just preview the command that is going to be ran. Default is
false
.
- Whether execute the request or just preview the command that is going to be ran. Default is
Returns
table
- Request results (HTTP client output)
Lua-cURL and Luarocks
To use cURL in the way the project currently requires it and have a better experience for both
rest.nvim
developers and users, Thunder Rest
will abandon the use of the cURL wrapper made in
plenary.nvim
and will embrace native bindings to libcURL
for Lua 5.1
.
rest.nvim
will also embrace Luarocks as a first-class citizen, due to a few reasons of which a couple are listed below:
- Ease of installation, true versioning management.
- Automatic management of dependencies and build steps.
If you want to dig a little deeper into the benefits of this decision and the problems it fixes for us as plugin developers and also for you as a user, please take a look at the following blog posts.
- Automatic install of neovim plugin dependencies
- Automatic install of neovim plugin dependencies (2)
- Publish your Neovim plugins to LuaRocks
Third-party clients
Thunder Rest
is going to provide an API to interact with third-party add-ons, written by the
rest-nvim community or any person. Something similar to nvim-cmp
completion sources.
You can checkout #144 for more context about the initial discussion for the implementation of third-party clients integrations.
This API is going to be built-in in the rest.nvim
source code. As an example, let’s suppose we are
adding support for official postman CLI:
We are going to add rest-postman
as a rest.nvim
dependency, where
rest-postman
add-on has the following structure:
lua/
└── rest-nvim/ # Root rest-nvim module directory
└── add-ons/ # Add-ons directory so we can require("rest-nvim.add-ons.foo")
└── postman/ # Postman CLI add-on module directory
├── init.lua # Postman CLI add-on module init file
└── utils.lua # Postman CLI add-on module utilities file
We can see that it does have a standard structure (rest-nvim/add-ons/client-name
), this is
essential as rest.nvim will look for an add-ons
module named like the third-party client
specified in the user config
function.
An add-on can internally contain any files additionally to the mandatory init.lua
file, as we
can see in the example.
Add-ons will also work with data extracted from HTTP documents by the rest.nvim
core parser, that
means add-ons are going to directly work with tree-sitter data. That way, add-ons maintainers will
only need to care about integrating the request data (request method and URL, headers, body, etc) in
the third-party client arguments when running it.
An user will be able to choose what third-party client he wants to use by installing add-ons
and then choosing the add-on on his rest.nvim config
function like this:
require("rest-nvim").config({
client = "postman", -- default is "curl"
})
This way we keep things simple for end users and rest.nvim
codebase, everything should work
automatically!
Third-party clients init file
As we already know, an init.lua
is a mandatory file for rest.nvim
add-ons as it does handle
the Lua module initialization and how it behaves. However, do we already know what should that
init.lua
file contain and return to rest.nvim
when requiring it? Well, this is what we are
going to learn now.
As you may know, rest.nvim
does expose commands to Neovim (e.g. :Rest run
) that triggers
different actions like: running request under the cursor, preview a request, etc. All
third-party clients integrations should keep an standard with the built-in cURL integration.
That means, add-ons must return several functions like run
and preview
as these
functions are called during :Rest
command executions.
Note: At the time of writing this section, integrations should return the functions mentioned in the Rest functions section.
Hello, Tree-Sitter!
Thunder Rest
will contain a parser
module. This module is going to handle all the HTTP documents
parsing logic. That means, rest-nvim.parser
task is extracting the information of the current
request under the cursor or all requests in the HTTP document, as requested by the end user. The
extracted information is the following:
- Request method and URL (e.g.
POST http://localhost:8080/api/v1/user/create
) - Headers
- Body
Note that not everything can be directly handled by tree-sitter. For example, document variables
(@url = http://localhost:8080
) and their expansion, this is why Thunder Rest
also helps tree-sitter
to parse the HTTP document using utility functions. This means that both work together in order to
get work done.
Extracted information is returned by rest-nvim.parser
as a structured Lua table, here is an
example:
HTTP document:
@API_PATH = api/v1
POST http://localhost:8080/{{API_PATH}}/users/create
Content-Type: application/json
{
"email": "example@mail.xyz",
"username": "NTBBloodbath",
"password": "3ncryptEd_p@s2w0rd"
}
rest-nvim.parser
output:
{
request = {
method = "POST",
url = "http://localhost:8080/api/v1/users/create",
},
headers = {
["Content-Type"] = "application/json",
},
body = {
email = "example@mail.xyz",
username = "NTBBloodbath",
password = "3ncryptEd_p@s2w0rd",
}
}
Please note how rest-nvim.parser
automatically expanded environment variables and returns
only what the HTTP clients needs in order to make the HTTP request.
Also, in case of parsing whole file with multiple requests, rest-nvim.parser
will return a
table with nested tables (array of tables) where parsed_document[1]
is the first request and
so on. Each nested table has exactly the same structure as the example output table above.
Parser
Parsing HTTP documents is something like rest.nvim
’s heart and brain — it does nothing without
parsing them. This is where we implement what the Hello, Tree-Sitter!
section does.
Right so we would usually think something like “we need to make a parser here”, and that is true, however, we already got a core Tree-Sitter parser for our HTTP documents. Sadly this is not enough for us, but why?
This is due to what rest.nvim
is capable of, this means, rest.nvim
can handle any kind of
variable and expand it (document-scoped
, environment
, etc). This is something tree-sitter
cannot directly do so we need to make what I like th call a layer on top of that tree-sitter
parser.
This layer is responsible for doing the dirty work that the tree-sitter parser cannot do by itself due to its nature. What is this dirty work, just reading and expanding environment variables in the document requests? Not really, it does much more than just that.
Variables
As we may know, our tree-sitter parser should be capable of understanding what a variable is and its scope so we can help our parser to expand it later so we can actually use the value it holds.
These variables can have different scopes that not only depends on the parser and we must also
scan some external files (e.g. .env
) in order to find their values!
So we can say that variables rules are the following:
- Document-scoped variables
- Variables that are declared directly in our HTTP document (
@API_PATH = api/v1
).
- Variables that are declared directly in our HTTP document (
- Environment variables
- System-wide or shell session variables (e.g.
$HOME
). - Variables coming from an environment file (e.g.
.env
).
- System-wide or shell session variables (e.g.
- Dynamic variables
- Special
rest.nvim
variables likeuuid
that gets evaluated on execution.
- Special
Contributing
rest.nvim
We highly encourage you to work with the following coding style in order to keep codebase consistency and also the following contribution workflow in order to be more organized.
Coding style
There is a .editorconfig
file in the root of rest.nvim
repository, everything should be
automatically set if you are using at least Neovim >= 0.9
, as
EditorConfig is builtin from that version. rest.nvim
uses the following style:
- 2 spaces indentation
- spaces over tabs
- double quotes over single quotes
- 120 characters as the maximum line length
- always use parentheses on function calls
There is also a stylua configuration file in case you want to use stylua formatter after you are done with your changes to the codebase.
Workflow
This is not mandatory, however, it does bring organization and a better readability to your codebase changes!
- A commit should do only one thing.
- e.g. A commit that fixes a bug should only fix that bug.
- A new feature/fix, a new branch.
- Every new feature or bug fix should be self-contained.
Important: verification and signing of commits will likely be required for future contributions.
Third-party HTTP clients integration
This point has not yet been completely decided, so a separate post will most likely be made when the time comes.
Special thanks
I want to say a huge thank you to all the people who have been donating lately to support rest.nvim, this wouldn’t have been possible if it weren’t for you. Stay tuned for changes! 💜
If you think I’ve missed something, please let me know through one of my contact forms at About me.