diff --git a/.github/workflows/docgen.yml b/.github/workflows/docgen.yml
new file mode 100644
index 00000000..1c3095e6
--- /dev/null
+++ b/.github/workflows/docgen.yml
@@ -0,0 +1,38 @@
+name: format
+
+on:
+  push:
+    branches: [main]
+    paths-ignore:
+      - ".github/**"
+      - "**.md"
+
+permissions:
+  contents: write
+  pull-requests: read
+
+jobs:
+  docgen:
+    runs-on: ubuntu-latest
+    name: Generate documentation
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Set up lemmy-help
+        uses: supplypike/setup-bin@v3
+        with:
+          uri: "https://github.com/numToStr/lemmy-help/releases/download/v0.11.0/lemmy-help-x86_64-unknown-linux-gnu.tar.gz"
+          name: lemmy-help
+          version: "0.11.0"
+      - name: Generate docs
+        run: "make docgen"
+      - name: Commit changes
+        uses: stefanzweifel/git-auto-commit-action@v5
+        with:
+          commit_message: "chore: regenerate documentation"
+          branch: ${{ github.ref }}
+      - name: Push formatted files
+        uses: ad-m/github-push-action@master
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          branch: ${{ github.ref }}
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index efbf7ef1..355087da 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -7,19 +7,23 @@ on:
       - ".github/**"
       - "**.md"
 
+permissions:
+  contents: write
+  pull-requests: write
+
 jobs:
   stylua:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - name: Setup and run stylua
-        uses: JohnnyMorganz/stylua-action@v3
+        uses: JohnnyMorganz/stylua-action@v4
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           version: v0.19.1
           args: --config-path=stylua.toml .
       - name: Commit changes
-        uses: stefanzweifel/git-auto-commit-action@v4
+        uses: stefanzweifel/git-auto-commit-action@v5
         with:
           commit_message: "chore: autoformat with stylua"
           branch: ${{ github.ref }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 89fe097f..b91f6916 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -10,8 +10,8 @@ jobs:
   luacheck:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: nebularg/actions-luacheck@v1.1.0
+      - uses: actions/checkout@v4
+      - uses: nebularg/actions-luacheck@v1.1.2
         with:
           files: 'lua/'
           config: 'https://raw.githubusercontent.com/NTBBloodbath/rest.nvim/main/.luacheckrc'
diff --git a/.github/workflows/luarocks.yml b/.github/workflows/luarocks.yml
index 30efb8d5..7d4dd718 100644
--- a/.github/workflows/luarocks.yml
+++ b/.github/workflows/luarocks.yml
@@ -2,7 +2,7 @@ name: Luarocks release
 
 on:
   push:
-    release:
+    releases:
       types:
         - created
     tags:
@@ -15,10 +15,20 @@ jobs:
     name: Luarocks upload
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0 # Required to count the commits
+      - name: Install build dependencies
+        run: sudo apt-get install -y libcurl4-gnutls-dev
       - name: Luarocks Upload
         uses: mrcjkb/luarocks-tag-release@v5
+        with:
+          dependencies: |
+            nvim-nio
+            lua-curl
+            mimetypes
+            xml2lua
+          extra_luarocks_args: |
+            CURL_INCDIR=/usr/include/x86_64-linux-gnu
         env:
           LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 067c97c4..9a9d9d1e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - main
+    workflow_dispatch:
 
 permissions:
   contents: write
@@ -16,5 +17,6 @@ jobs:
     steps:
       - uses: google-github-actions/release-please-action@v3
         with:
+          token: ${{ secrets.CI_TOKEN }}
           release-type: simple
           package-name: rest.nvim
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 06659264..00000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: test
-
-on: [push, pull_request]
-
-jobs:
-  tests:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - uses: cachix/install-nix-action@v21
-      with:
-        nix_path: nixpkgs=channel:nixos-unstable
-    - run: |
-        nix develop .#ci -c make test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e15ec295..cb826316 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,5 @@
 # Changelog
 
-## [1.2.1](https://github.com/rest-nvim/rest.nvim/compare/v1.2.0...v1.2.1) (2024-03-15)
-
-
-### Bug Fixes
-
-* get json content type for custom content types ([#297](https://github.com/rest-nvim/rest.nvim/issues/297)) ([91badd4](https://github.com/rest-nvim/rest.nvim/commit/91badd46c60df6bd9800c809056af2d80d33da4c))
-
-## [1.2.0](https://github.com/rest-nvim/rest.nvim/compare/v1.1.0...v1.2.0) (2024-03-05)
-
-
-### Features
-
-* add pre-script configuration ([#287](https://github.com/rest-nvim/rest.nvim/issues/287)) ([2bb9570](https://github.com/rest-nvim/rest.nvim/commit/2bb957091ff8ddf1945308fae2925ce76f93b9ed))
-
 ## [1.1.0](https://github.com/rest-nvim/rest.nvim/compare/v1.0.1...v1.1.0) (2024-02-12)
 
 
diff --git a/LICENSE b/LICENSE
index 7f2b3607..f288702d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,19 +1,674 @@
-Copyright (c) 2021 NTBBloodbath
-
-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.
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/Makefile b/Makefile
index 3a526153..56b63322 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,15 @@
+.PHONY: lint format docgen
+.SILENT: docgen
+
 lint:
 	luacheck .
 
 format:
 	stylua .
 
-test:
-	# possible args to test_directory: sequential=true,keep_going=false
-	# minimal.vim is generated when entering the flake, aka `nix develop ./contrib`
-	nvim --headless -u minimal.vim -c "lua require('plenary.test_harness').test_directory('.', {minimal_init='minimal.vim'})"
-
+docgen:
+	lemmy-help lua/rest-nvim/client/curl.lua > doc/rest-nvim-curl.txt
+	lemmy-help lua/rest-nvim/commands.lua > doc/rest-nvim-commands.txt
+	lemmy-help lua/rest-nvim/config/init.lua > doc/rest-nvim-config.txt
+	lemmy-help lua/rest-nvim/parser/dynamic_vars.lua lua/rest-nvim/parser/env_vars.lua lua/rest-nvim/parser/script_vars.lua lua/rest-nvim/parser/init.lua > doc/rest-nvim-parser.txt
+	lemmy-help lua/rest-nvim/api.lua lua/rest-nvim/utils.lua lua/rest-nvim/functions.lua lua/rest-nvim/logger.lua lua/rest-nvim/result/init.lua lua/rest-nvim/result/winbar.lua lua/rest-nvim/result/help.lua > doc/rest-nvim-api.txt
diff --git a/README.md b/README.md
index d56ebe8c..951a2cee 100644
--- a/README.md
+++ b/README.md
@@ -3,226 +3,289 @@
 # rest.nvim
 
 ![License](https://img.shields.io/github/license/NTBBloodbath/rest.nvim?style=for-the-badge)
-![Neovim version](https://img.shields.io/badge/Neovim-0.5-5ba246?style=for-the-badge&logo=neovim)
-![Matrix](https://img.shields.io/matrix/rest.nvim%3Amatrix.org?server_fqdn=matrix.org&style=for-the-badge&logo=element&label=Matrix&color=55b394&link=https%3A%2F%2Fmatrix.to%2F%23%2F%23rest.nvim%3Amatrix.org)
+![Neovim version](https://img.shields.io/badge/Neovim-0.9.2-5ba246?style=for-the-badge&logo=neovim)
+[![LuaRocks](https://img.shields.io/luarocks/v/teto/rest.nvim?style=for-the-badge&logo=lua&color=blue)](https://luarocks.org/modules/teto/rest.nvim)
+[![Discord](https://img.shields.io/badge/discord-join-7289da?style=for-the-badge&logo=discord)](https://discord.gg/AcXkuXKj7C)
+[![Matrix](https://img.shields.io/matrix/rest.nvim%3Amatrix.org?server_fqdn=matrix.org&style=for-the-badge&logo=element&label=Matrix&color=55b394&link=https%3A%2F%2Fmatrix.to%2F%23%2F%23rest.nvim%3Amatrix.org)](https://matrix.to/#/#rest.nvim:matrix.org)
 
 [Features](#features) • [Install](#install) • [Usage](#usage) • [Contribute](#contribute)
 
-![Demo](./assets/demo.png)
+![Demo](https://github.com/rest-nvim/rest.nvim/assets/36456999/e9b536a5-f7b2-4cd8-88fb-fdc5409dd2a4)
 
 </div>
 
 ---
 
-A fast Neovim http client written in Lua.
+A very fast, powerful, extensible and asynchronous Neovim HTTP client written in Lua.
 
-`rest.nvim` makes use of a curl wrapper made in pure Lua by [tami5] and implemented
-in `plenary.nvim` so, in other words, `rest.nvim` is a curl wrapper so you don't
-have to leave Neovim!
+`rest.nvim` by default makes use of native [cURL](https://curl.se/) bindings. In this way, you get
+absolutely all the power that cURL provides from the comfort of our editor just by using a keybind
+and without wasting the precious resources of your machine.
 
-> **IMPORTANT:** If you are facing issues, please [report them](https://github.com/rest-nvim/rest.nvim/issues/new)
+In addition to this, you can also write integrations with external HTTP clients, such as the postman
+CLI. For more information on this, please see this [blog post](https://amartin.codeberg.page/posts/first-look-at-thunder-rest/#third-party-clients).
 
-## Notices
-
-- **2023-07-12**: tagged 0.2 release before changes for 0.10 compatibility
-- **2021-11-04**: HTTP Tree-Sitter parser now depends on JSON parser for the JSON bodies detection,
-  please install it too.
-- **2021-08-26**: We have deleted the syntax file for HTTP files to start using the tree-sitter parser instead,
-  please see [Tree-Sitter parser](#tree-sitter-parser) section for more information.
-- **2021-07-01**: Now for getting syntax highlighting in http files you should
-  add a `require('rest-nvim').setup()` to your `rest.nvim` setup, refer to [packer.nvim](#packernvim).
-  This breaking change should allow lazy-loading of `rest.nvim`.
+> [!IMPORTANT]
+>
+> If you are facing issues, please [report them](https://github.com/rest-nvim/rest.nvim/issues/new) so we can work in a fix together :)
 
 ## Features
 
 - Easy to use
-- Fast execution time
-- Run request under cursor
-- Syntax highlight for http files and output
-- Possibility of using environment variables in http files
+- Friendly and organized request results window
+- Fast runtime with statistics about your request
+- Set custom pre-request and post-request hooks to dynamically interact with the data
+- Easily set environment variables based on the response to re-use the data later
+- Tree-sitter based parsing and syntax highlighting for speed and perfect accuracy
+- Possibility of using dynamic/environment variables and Lua scripting in HTTP files
 
 ## Install
 
-> **WARNING:** rest.nvim requires Neovim >= 0.5 to work.
+> [!NOTE]
+>
+> rest.nvim requires Neovim >= 0.9.2 to work.
 
 ### Dependencies
 
 - System-wide
-  - curl
-- Optional [can be changed, see config below]
-  - jq   (to format JSON output)
-  - tidy (to format HTML output)
-- Other plugins
-  - [plenary.nvim](https://github.com/nvim-lua/plenary.nvim)
+  - `Python` (only if you are using `packer.nvim` or `lazy.nvim` plus `luarocks.nvim` for the installation)
+  - `cURL` development headers (usually called `libcurl-dev` or `libcurl-devel` depending on your Linux distribution)
+- Optional [can be changed, see config below](#default-configuration)
+  - `jq`   (to format JSON output)
+  - `tidy` (to format HTML output)
+
+> [!NOTE]
+>
+> 1. Python will be unnecessary once `luarocks.nvim` gets rid of it as a dependency in the `go-away-python` branch.
+>
+> 2. I will be working on making a binary rock of `Lua-cURL` so that the `cURL` development headers are not
+> necessary for the installation process.
+
+### [rocks.nvim](https://github.com/nvim-neorocks/rocks.nvim) (recommended)
 
-### packer.nvim
+```vim
+:Rocks install rest.nvim
+```
+
+### [packer.nvim](https://github.com/wbthomason/packer.nvim)
 
 ```lua
 use {
   "rest-nvim/rest.nvim",
-  requires = { "nvim-lua/plenary.nvim" },
+  rocks = { "lua-curl", "nvim-nio", "mimetypes", "xml2lua" },
   config = function()
-    require("rest-nvim").setup({
-      -- Open request results in a horizontal split
-      result_split_horizontal = false,
-      -- Keep the http file buffer above|left when split horizontal|vertical
-      result_split_in_place = false,
-      -- stay in current windows (.http file) or change to results window (default)
-      stay_in_current_window_after_split = false,
-      -- Skip SSL verification, useful for unknown certificates
-      skip_ssl_verification = false,
-      -- Encode URL before making request
-      encode_url = true,
-      -- Highlight request on run
-      highlight = {
-        enabled = true,
-        timeout = 150,
-      },
-      result = {
-        -- toggle showing URL, HTTP info, headers at top the of result window
-        show_url = true,
-        -- show the generated curl command in case you want to launch
-        -- the same request via the terminal (can be verbose)
-        show_curl_command = false,
-        show_http_info = true,
-        show_headers = true,
-        -- table of curl `--write-out` variables or false if disabled
-        -- for more granular control see Statistics Spec
-        show_statistics = false,
-        -- executables or functions for formatting response body [optional]
-        -- set them to false if you want to disable them
-        formatters = {
-          json = "jq",
-          html = function(body)
-            return vim.fn.system({"tidy", "-i", "-q", "-"}, body)
-          end
-        },
-      },
-      -- Jump to request line on run
-      jump_to_request = false,
-      env_file = '.env',
-      -- for telescope select
-      env_pattern = "\\.env$",
-      env_edit_command = "tabedit",
-      custom_dynamic_variables = {},
-      yank_dry_run = true,
-      search_back = true,
-    })
-  end
+    require("rest-nvim").setup()
+  end,
 }
 ```
 
 ### [lazy.nvim](https://github.com/folke/lazy.nvim)
 
 ```lua
--- plugins/rest.lua
-return {
-   "rest-nvim/rest.nvim",
-   dependencies = { { "nvim-lua/plenary.nvim" } },
-   ft = 'http',
-   config = function()
-     require("rest-nvim").setup({
-       --- Get the same options from Packer setup
-    })
-  end
+{
+  "vhyrro/luarocks.nvim",
+  config = function()
+    require("luarocks").setup({})
+  end,
+},
+{
+  "rest-nvim/rest.nvim",
+  ft = "http",
+  dependencies = { "luarocks.nvim" },
+  config = function()
+    require("rest-nvim").setup()
+  end,
+}
+```
+
+> [!NOTE]
+>
+> There's a `build.lua` file in the repository that `lazy.nvim` will find and source to install the
+> luarocks dependencies for you by using `luarocks.nvim`.
+
+### Default configuration
+
+This is the default configuration of `rest.nvim`, it is fully documented and typed internally so you
+get a good experience during autocompletion :)
+
+> [!NOTE]
+>
+> You can also check out `:h rest-nvim.config` for documentation.
+
+```lua
+local default_config = {
+  client = "curl",
+  env_file = ".env",
+  env_pattern = "\\.env$",
+  env_edit_command = "tabedit",
+  encode_url = true,
+  skip_ssl_verification = false,
+  custom_dynamic_variables = {},
+  logs = {
+    level = "info",
+    save = true,
+  },
+  result = {
+    split = {
+      horizontal = false,
+      in_place = false,
+      stay_in_current_window_after_split = true,
+    },
+    behavior = {
+      decode_url = true,
+      show_info = {
+        url = true,
+        headers = true,
+        http_info = true,
+        curl_command = true,
+      },
+      statistics = {
+        enable = true,
+        ---@see https://curl.se/libcurl/c/curl_easy_getinfo.html
+        stats = {
+          { "total_time", title = "Time taken:" },
+          { "size_download_t", title = "Download size:" },
+        },
+      },
+      formatters = {
+        json = "jq",
+        html = function(body)
+          if vim.fn.executable("tidy") == 0 then
+            return body, { found = false, name = "tidy" }
+          end
+          local fmt_body = vim.fn.system({
+            "tidy",
+            "-i",
+            "-q",
+            "--tidy-mark",      "no",
+            "--show-body-only", "auto",
+            "--show-errors",    "0",
+            "--show-warnings",  "0",
+            "-",
+          }, body):gsub("\n$", "")
+
+          return fmt_body, { found = true, name = "tidy" }
+        end,
+      },
+    },
+  },
+  highlight = {
+    enable = true,
+    timeout = 750,
+  },
+  ---Example:
+  ---
+  ---```lua
+  ---keybinds = {
+  ---  {
+  ---    "<localleader>rr", ":Rest run", "Run request under the cursor",
+  ---  },
+  ---  {
+  ---    "<localleader>rl", ":Rest run last", "Re-run latest request",
+  ---  },
+  ---}
+  ---
+  ---```
+  ---@see vim.keymap.set
+  keybinds = {},
 }
 ```
 
-### Tree-Sitter parser
+### Tree-Sitter parsing
 
-We are using a Tree-Sitter parser for our HTTP files, in order to get the correct syntax highlighting
-for HTTP files (including JSON bodies) you should add the following into your `ensure_installed` table
-in your tree-sitter setup.
+`rest.nvim` uses tree-sitter as a first-class citizen, so it will not work if the required parsers are
+not installed. These parsers are as follows and you can add them to your `ensure_installed` table
+in your `nvim-treesitter` configuration.
 
 ```lua
-ensure_installed = { "http", "json" }
+ensure_installed = { "lua", "xml", "http", "json", "graphql" }
 ```
 
-Or manually run `:TSInstall http json`.
+Or manually run `:TSInstall lua xml http json graphql`.
 
 ## Keybindings
 
 By default `rest.nvim` does not have any key mappings so you will not have
 conflicts with any of your existing ones.
 
-To run `rest.nvim` you should map the following commands:
+However, `rest.nvim` exposes a `:Rest` command in HTTP files that you can use to create your
+keybinds easily. For example:
+
+```lua
+keybinds = {
+  {
+    "<localleader>rr", ":Rest run", "Run request under the cursor",
+  },
+  {
+    "<localleader>rl", ":Rest run last", "Re-run latest request",
+  },
+}
+```
+
+You can still also use the legacy `<Plug>RestNvim` commands for mappings:
 - `<Plug>RestNvim`, run the request under the cursor
-- `<Plug>RestNvimPreview`, preview the request cURL command
 - `<Plug>RestNvimLast`, re-run the last request
 
-## Settings
-
-- `result_split_horizontal` opens result on a horizontal split (default opens
-    on vertical)
-- `result_split_in_place` opens result below|right on horizontal|vertical split
-    (default opens top|left on horizontal|vertical split)
-- `skip_ssl_verification` passes the `-k` flag to cURL in order to skip SSL verification,
-    useful when using unknown certificates
-- `encode_url` flag to encode the URL before making request
-- `highlight` allows to enable and configure the highlighting of the selected request when send,
-- `jump_to_request` moves the cursor to the selected request line when send,
-- `env_file` specifies file name that consist environment variables (default: .env)
-- `custom_dynamic_variables` allows to extend or overwrite built-in dynamic variable functions
-    (default: {})
-
-### Statistics Spec
-
-| Property | Type               | Description                                            |
-| :------- | :----------------- | :----------------------------------------------------- |
-| [1]      | string             | `--write-out` variable name, see `man curl`. Required. |
-| title    | string             | Replaces the variable name in the output if defined.   |
-| type     | string or function | Specifies type transformation for the output value. Default transformers are `time` and `size`. Can also be a function which takes the value as a parameter and returns a string. |
+> [!NOTE]
+>
+> 1. `<Plug>RestNvimPreview` has been removed, as we can no longer implement it with the current
+>    cURL implementation.
+>
+> 2. The legacy `<Plug>` mappings will raise a deprecation warning suggesting you to switch to
+>    the `:Rest` command, as they are going to be completely removed in the next version.
 
 ## Usage
 
 Create a new http file or open an existing one and place the cursor over the
-request method (e.g. `GET`) and run `rest.nvim`.
+request and run the <kbd>:Rest run</kbd> command.
 
-> **NOTES**:
+> [!NOTE]
 >
-> 1. `rest.nvim` follows the RFC 2616 request format so any other
->    http file should work without problems.
+> 1. You can find examples of use in the [tests](./tests) directory.
 >
-> 2. You can find examples of use in [tests](./tests)
-
----
+> 2. `rest.nvim` supports multiple HTTP requests in one file. It selects the
+>    request in the current cursor line, no matters the position as long as
+>    the cursor is on a request tree-sitter node.
 
-### Debug
 
+---
 
-Run `export DEBUG_PLENARY="debug"` before starting nvim. Logs will appear most
-likely in ~/.cache/nvim/rest.nvim.log
+### Telescope Extension
 
-## Telescope Extension
+`rest.nvim` provides a [telescope.nvim] extension to select the environment variables file,
+you can load and use it with the following snippet:
 
 ```lua
-
 -- first load extension
 require("telescope").load_extension("rest")
--- then use it
+-- then use it, you can also use the `:Telescope rest select_env` command
 require("telescope").extensions.rest.select_env()
-
 ```
 
+If running Ubuntu or Debian based systems you might need to run `ln -s $(which fdfind) ~/.local/bin/fd` to get extension to work. This is becuase extension runs the [fd](https://github.com/sharkdp/fd?tab=readme-ov-file#installation) command.
+
+Here is a preview of the extension working :)
+
+![telescope rest extension demo](https://github.com/rest-nvim/rest.nvim/assets/36456999/a810954f-b45c-44ee-854d-94039de8e2fc)
+
 ### Mappings
 
-- Enter: Select Env file
-- Ctrl+O: Edit Env file
+- <kbd>Enter</kbd>: Select Env file
+- <kbd>Ctrl + O</kbd>: Edit Env file
 
 ### Config
 
-- env_pattern: For env file pattern
-- env_edit_command: For env file edit command
-
-If running Ubuntu or Debian based systems you might need to run `ln -s $(which fdfind) ~/.local/bin/fd` to get extension to work. This is becuase extension runs the [fd](https://github.com/sharkdp/fd?tab=readme-ov-file#installation) command.
+- `env_pattern`: For env file pattern
+- `env_edit_command`: For env file edit command
 
 ## Lualine
 
 We also have lualine component to get what env file you select!
-And dont't worry, it will only show up under http files.
+
+And dont't worry, it will only show up under HTTP files.
 
 ```lua
--- Juse add a component in your lualine config
+-- Just add a component in your lualine config
 {
   sections = {
     lualine_x = {
@@ -231,7 +294,7 @@ And dont't worry, it will only show up under http files.
   }
 }
 
--- To custom icon and color
+-- To use a custom icon and color
 {
   sections = {
     lualine_x = {
@@ -245,16 +308,23 @@ And dont't worry, it will only show up under http files.
 }
 ```
 
+Here is a preview of the component working :)
+
+![lualine component demo](https://github.com/rest-nvim/rest.nvim/assets/81607010/cf4bb327-61aa-494c-84a5-82f5ee21004f)
+
 ## Contribute
 
 1. Fork it (https://github.com/rest-nvim/rest.nvim/fork)
 2. Create your feature branch (<kbd>git checkout -b my-new-feature</kbd>)
-3. Commit your changes (<kbd>git commit -am 'Add some feature'</kbd>)
-4. Push to the branch (<kbd>git push origin my-new-feature</kbd>)
+3. Commit your changes (<kbd>git commit -am 'feat: add some feature'</kbd>)
+4. Push to the branch (<kbd>git push -u origin my-new-feature</kbd>)
 5. Create a new Pull Request
 
-To run the tests, enter a nix shell with `nix develop ./contrib`, then run `make
-test`.
+> [!IMPORTANT]
+>
+> rest.nvim uses [semantic commits](https://www.conventionalcommits.org/en/v1.0.0/) that adhere to
+> semantic versioning and these help with automatic releases, please use this type of convention
+> when submitting changes to the project.
 
 ## Related software
 
@@ -265,6 +335,4 @@ test`.
 
 ## License
 
-rest.nvim is [MIT Licensed](./LICENSE).
-
-[tami5]: https://github.com/tami5
+rest.nvim is [GPLv3 Licensed](./LICENSE).
diff --git a/after/queries/http/highlights.scm b/after/queries/http/highlights.scm
index 3b1ac926..dfb5a5e9 100644
--- a/after/queries/http/highlights.scm
+++ b/after/queries/http/highlights.scm
@@ -1,71 +1,77 @@
 ; Keywords
-
-(scheme) @namespace
+(scheme) @module
 
 ; Methods
-
-(method) @method
+(method) @function.method
 
 ; Constants
-
 (const_spec) @constant
 
 ; Headers
-
 (header
   name: (name) @constant)
 
 ; Variables
+(variable_declaration
+  name: (identifier) @variable)
 
-(identifier) @variable
+(variable_declaration
+  value: (number) @number)
 
-; Fields
+(variable_declaration
+  value: (boolean) @boolean)
 
-(pair name: (identifier) @field)
+(variable_declaration
+  value: (string) @string)
+
+; Fields
+(pair
+  name: (identifier) @variable.member)
 
 ; URL / Host
-(host) @text.uri
-(host (identifier) @text.uri)
-(path (identifier) @text.uri)
+(host) @string.special.url
 
-; Parameters
+(host
+  (identifier) @string.special.url)
 
-(query_param (key) @parameter)
+(path
+  (identifier) @string.special.url)
 
-; Operators
+; Parameters
+(query_param
+  (key) @variable.parameter)
 
+; Operators
 [
   "="
   "?"
   "&"
   "@"
+  "<"
 ] @operator
 
 ; Literals
+(target_url) @string.special.url
 
-(string) @string
+(http_version) @constant
 
-(target_url) @text.uri
+(string) @string
 
 (number) @number
 
-; (boolean) @boolean
-
-(null) @constant.builtin
+(boolean) @boolean
 
 ; Punctuation
+[
+  "{{"
+  "}}"
+] @punctuation.bracket
 
-[ "{{" "}}" ] @punctuation.bracket
+":" @punctuation.delimiter
 
-[
-  ":"
-] @punctuation.delimiter
+; external JSON body
+(external_body
+  file_path: (path) @string.special.path)
 
 ; Comments
-
 (comment) @comment @spell
-
-; Errors
-
-(ERROR) @error
-
diff --git a/after/queries/http/injections.scm b/after/queries/http/injections.scm
index 16bc175c..68268afc 100644
--- a/after/queries/http/injections.scm
+++ b/after/queries/http/injections.scm
@@ -1,7 +1,17 @@
-; (comment) @comment
+; Comments
+((comment) @injection.content
+  (#set! injection.language "comment"))
 
-(json_body) @json
+; Body
+((json_body) @injection.content
+  (#set! injection.language "json"))
 
-; (xml_body) @xml
+((xml_body) @injection.content
+  (#set! injection.language "xml"))
 
-; (graphql_body) @graphql Not used as of now..
+((graphql_body) @injection.content
+  (#set! injection.language "graphql"))
+
+; Lua scripting
+((script_variable) @injection.content
+  (#set! injection.language "lua"))
diff --git a/build.lua b/build.lua
new file mode 100644
index 00000000..c7c80ed0
--- /dev/null
+++ b/build.lua
@@ -0,0 +1,20 @@
+-- This build.lua exists to bridge luarocks installation for lazy.nvim users.
+-- It's main purposes are:
+-- - Shelling out to luarocks.nvim for installation
+-- - Installing rest's dependencies as rocks
+--
+-- Important note: we execute the build code in a vim.schedule
+-- to defer the execution and ensure that the runtimepath is appropriately set.
+
+vim.schedule(function()
+  local ok, luarocks = pcall(require, "luarocks.rocks")
+
+  assert(ok, "Unable to install rest.nvim: required dependency `vhyrro/luarocks.nvim` not found!")
+
+  luarocks.ensure({
+    "nvim-nio ~> 1.7",
+    "lua-curl ~> 0.3",
+    "mimetypes ~> 1.0",
+    "xml2lua ~> 1.5",
+  })
+end)
diff --git a/doc/rest-nvim-api.txt b/doc/rest-nvim-api.txt
new file mode 100644
index 00000000..d709b031
--- /dev/null
+++ b/doc/rest-nvim-api.txt
@@ -0,0 +1,343 @@
+==============================================================================
+rest.nvim Lua API                                                *rest-nvim.api*
+
+
+The Lua API for rest.nvim
+Intended for use by third-party modules that extend its functionalities.
+
+
+api.VERSION                                                        *api.VERSION*
+    rest.nvim API version, equals to the current rest.nvim version. Meant to be used by modules later
+
+    Type: ~
+        (string)
+
+    See: ~
+        |vim.version|
+
+
+api.namespace                                                    *api.namespace*
+    rest.nvim namespace used for buffer highlights
+
+    Type: ~
+        (number)
+
+    See: ~
+        |vim.api.nvim_create_namespace|
+
+
+                                                     *api.register_rest_autocmd*
+api.register_rest_autocmd({events}, {cb}, {description})
+
+
+    Parameters: ~
+        {events}       (string[])                Autocommand events, see `:h events`
+        {cb}           (string|fun(args:table))  Autocommand lua callback, runs a Vimscript command instead if it is a `string`
+        {description}  (string)                  Autocommand description
+
+
+                                                  *api.register_rest_subcommand*
+api.register_rest_subcommand({name}, {cmd})
+    Register a new `:Rest` subcommand
+
+    Parameters: ~
+        {name}  (string)   The name of the subcommand to register
+        {cmd}   (RestCmd)
+
+
+                                                     *api.register_rest_keybind*
+api.register_rest_keybind({mode}, {lhs}, {cmd}, {opts})
+
+
+    Parameters: ~
+        {mode}  (string)  Keybind mode
+        {lhs}   (string)  Keybind trigger
+        {cmd}   (string)  Command to be run
+        {opts}  (table)   Keybind options
+
+
+==============================================================================
+rest.nvim utilities                                            *rest-nvim.utils*
+
+
+ rest.nvim utility functions
+
+
+utils.escape({str})                                               *utils.escape*
+    Encodes a string into its escaped hexadecimal representation
+    taken from Lua Socket and added underscore to ignore
+
+    Parameters: ~
+        {str}  (string)  Binary string to be encoded
+
+    Returns: ~
+        (string)
+
+
+utils.file_exists({path})                                    *utils.file_exists*
+    Check if a file exists in the given `path`
+
+    Parameters: ~
+        {path}  (string)  file path
+
+    Returns: ~
+        (boolean)
+
+
+utils.read_file({path})                                        *utils.read_file*
+    Read a file if it exists
+
+    Parameters: ~
+        {path}  (string)  file path
+
+    Returns: ~
+        (string)
+
+
+                                                               *utils.highlight*
+utils.highlight({bufnr}, {start}, {end_}, {ns})
+    Highlight a request
+
+    Parameters: ~
+        {bufnr}  (number)  Buffer handler ID
+        {start}  (number)  Request tree-sitter node start
+        {end_}   (number)  Request tree-sitter node end
+        {ns}     (number)  rest.nvim Neovim namespace
+
+
+==============================================================================
+rest.nvim functions                                        *rest-nvim.functions*
+
+
+ rest.nvim functions
+
+
+functions.exec({scope})                                         *functions.exec*
+    Execute 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.
+
+    Parameters: ~
+        {scope}  (string)  Defines the request execution scope. Can be: `last`, `cursor` (default) or `document`
+
+
+functions.find_env_files()                            *functions.find_env_files*
+    Find a list of environment files starting from the current directory
+
+    Returns: ~
+        (string[])  variable files path
+
+
+functions.env({action}, {path})                                  *functions.env*
+    Manage the environment file that is currently in use while running requests
+
+    If you choose to `set` the environment, you must provide a `path` to the environment file.
+
+    Parameters: ~
+        {action}  (string|nil)  Determines the action to be taken. Can be: `set` or `show` (default)
+        {path}    (string|nil)  Path to the environment variables file
+
+
+functions.cycle_result_pane({cycle})               *functions.cycle_result_pane*
+    Cycle through the results buffer winbar panes
+
+    Parameters: ~
+        {cycle}  (string)  Cycle direction, can be: `"next"` or `"prev"`
+
+
+==============================================================================
+rest.nvim logger                                              *rest-nvim.logger*
+
+
+Logging library for rest.nvim, slightly inspired by rmagatti/logger.nvim
+Intended for use by internal and third-party modules.
+
+Default logger instance is made during the `setup` and can be accessed
+by anyone through the `_G._rest_nvim.logger` configuration field
+that is set automatically.
+
+------------------------------------------------------------------------------
+
+Usage:
+
+```lua
+local logger = require("rest-nvim.logger"):new({ level = "debug" })
+
+logger:set_log_level("info")
+
+logger:info("This is an info log")
+ -- [rest.nvim] INFO: This is an info log
+```
+
+
+Logger                                                                  *Logger*
+
+
+LoggerLevels                                                      *LoggerLevels*
+
+
+LoggerConfig                                                      *LoggerConfig*
+
+    Fields: ~
+        {level_name}  (string)   Logging level name. Default is `"info"`
+        {save_logs}   (boolean)  Whether to save log messages into a `.log` file. Default is `true`
+
+
+logger:new({opts})                                                  *logger:new*
+    Create a new logger instance
+
+    Parameters: ~
+        {opts}  (LoggerConfig)  Logger configuration
+
+    Returns: ~
+        (Logger)
+
+
+logger:set_log_level({level})                             *logger:set_log_level*
+    Set the log level for the logger
+
+    Parameters: ~
+        {level}  (string)  New logging level
+
+    See: ~
+        |vim.log.levels|
+
+
+logger:trace({msg})                                               *logger:trace*
+    Log a trace message
+
+    Parameters: ~
+        {msg}  (string)  Log message
+
+
+logger:debug({msg})                                               *logger:debug*
+    Log a debug message
+
+    Parameters: ~
+        {msg}  (string)  Log message
+
+
+logger:info({msg})                                                 *logger:info*
+    Log an info message
+
+    Parameters: ~
+        {msg}  (string)  Log message
+
+
+logger:warn({msg})                                                 *logger:warn*
+    Log a warning message
+
+    Parameters: ~
+        {msg}  (string)  Log message
+
+
+logger:error({msg})                                               *logger:error*
+    Log an error message
+
+    Parameters: ~
+        {msg}  (string)  Log message
+
+
+==============================================================================
+rest.nvim result buffer                                       *rest-nvim.result*
+
+
+ rest.nvim result buffer handling
+
+
+result.bufnr                                                      *result.bufnr*
+    Results buffer handler number
+
+    Type: ~
+        (number|nil)
+
+
+result.get_or_create_buf()                            *result.get_or_create_buf*
+
+    Returns: ~
+        (number)  handler number
+
+
+result.write_block()                                        *result.write_block*
+
+    See: ~
+        |vim.api.nvim_buf_set_lines|
+
+
+result.display_buf({bufnr}, {stats})                        *result.display_buf*
+    Display results buffer window
+
+    Parameters: ~
+        {bufnr}  (number)  The target buffer
+        {stats}  (table)   Request statistics
+
+
+result.write_res({bufnr}, {res})                              *result.write_res*
+    Write request results in the given buffer and display it
+
+    Parameters: ~
+        {bufnr}  (number)  The target buffer
+        {res}    (table)   Request results
+
+
+==============================================================================
+rest.nvim result buffer winbar add-on                  *rest-nvim.result.winbar*
+
+
+ rest.nvim result buffer winbar
+
+
+winbar.current_pane_index                            *winbar.current_pane_index*
+    Current pane index in the results window winbar
+
+    Type: ~
+        (number)
+
+
+winbar.get_content({stats})                                 *winbar.get_content*
+    Create the winbar contents and return them
+
+    Parameters: ~
+        {stats}  (table)  Request statistics
+
+    Returns: ~
+        (string)
+
+
+ResultPane                                                          *ResultPane*
+
+    Fields: ~
+        {name}      (string)    Pane name
+        {contents}  (string[])  Pane contents
+
+
+winbar.set_hl()                                                  *winbar.set_hl*
+    Set the results window winbar highlighting groups
+
+
+winbar.set_pane({selected})                                    *winbar.set_pane*
+    Select the winbar panel based on the pane index and set the pane contents
+
+    If the pane index is higher than 4 or lower than 1, it will cycle through
+    the panes, e.g. >= 5 gets converted to 1 and <= 0 gets converted to 4
+
+    Parameters: ~
+        {selected}  (number)  winbar pane index
+
+
+==============================================================================
+rest.nvim result buffer help                             *rest-nvim.result.help*
+
+
+ rest.nvim result buffer help window handling
+
+
+help.open()                                                          *help.open*
+    Open the request results help window
+
+
+help.close()                                                        *help.close*
+    Close the request results help window
+
+
+vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/doc/rest-nvim-commands.txt b/doc/rest-nvim-commands.txt
new file mode 100644
index 00000000..fa13bc8c
--- /dev/null
+++ b/doc/rest-nvim-commands.txt
@@ -0,0 +1,37 @@
+==============================================================================
+rest.nvim commands                                          *rest-nvim.commands*
+
+
+ `:Rest {command {args?}}`
+
+ command                         action
+------------------------------------------------------------------------------
+
+ run {scope?}                    Execute one or several HTTP requests depending
+                                 on given `scope`. This scope can be either `last`,
+                                 `cursor` (default) or `document`.
+
+ last                            Re-run the last executed request, alias to `run last`
+                                 to retain backwards compatibility with the old keybinds
+                                 layout.
+
+ logs                            Open the rest.nvim logs file in a new tab.
+
+ env {action?} {path?}           Manage the environment file that is currently in use while
+                                 running requests. If you choose to `set` the environment,
+                                 you must provide a `path` to the environment file. The
+                                 default action is `show`, which displays the current
+                                 environment file path.
+
+ result {direction?}             Cycle through the results buffer winbar panes. The cycle
+                                 direction can be either `next` or `prev`.
+
+
+RestCmd                                                                *RestCmd*
+
+    Fields: ~
+        {impl}       (fun(args:string[],opts:vim.api.keyset.user_command))  The command implementation
+        {complete?}  (fun(subcmd_arg_lead:string):string[])                 Command completions callback, taking the lead of the subcommand's argument
+
+
+vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/doc/rest-nvim-config.txt b/doc/rest-nvim-config.txt
new file mode 100644
index 00000000..b529557d
--- /dev/null
+++ b/doc/rest-nvim-config.txt
@@ -0,0 +1,90 @@
+==============================================================================
+rest.nvim configuration                                       *rest-nvim.config*
+
+
+ rest.nvim configuration options
+
+
+RestConfigDebug                                                *RestConfigDebug*
+
+    Fields: ~
+        {unrecognized_configs}  (string[])  Unrecognized configuration options
+
+
+RestConfigLogs                                                  *RestConfigLogs*
+
+    Fields: ~
+        {level}  (string)   The logging level name, see `:h vim.log.levels`. Default is `"info"`
+        {save}   (boolean)  Whether to save log messages into a `.log` file. Default is `true`
+
+
+RestConfigResult                                              *RestConfigResult*
+
+    Fields: ~
+        {split}     (RestConfigResultSplit)     Result split window behavior
+        {behavior}  (RestConfigResultBehavior)  Result buffer behavior
+
+
+RestConfigResultSplit                                    *RestConfigResultSplit*
+
+    Fields: ~
+        {horizontal}                          (boolean)  Open request results in a horizontal split
+        {in_place}                            (boolean)  Keep the HTTP file buffer above|left when split horizontal|vertical
+        {stay_in_current_window_after_split}  (boolean)  Stay in the current window (HTTP file) or change the focus to the results window
+
+
+RestConfigResultBehavior                              *RestConfigResultBehavior*
+
+    Fields: ~
+        {show_info}   (RestConfigResultInfo)        Request results information
+        {decode_url}  (boolean)                     Whether to decode the request URL query parameters to improve readability
+        {statistics}  (RestConfigResultStats)       Request results statistics
+        {formatters}  (RestConfigResultFormatters)  Formatters for the request results body. If the formatter is a function it should return two values, the formatted body and a table containing two values `found` (whether the formatter has been found or not) and `name` (the formatter name)
+
+
+RestConfigResultInfo                                      *RestConfigResultInfo*
+
+    Fields: ~
+        {url}           (boolean)  Display the request URL
+        {headers}       (boolean)  Display the request headers
+        {http_info}     (boolean)  Display the request HTTP information
+        {curl_command}  (boolean)  Display the cURL command that was used for the request
+
+
+RestConfigResultStats                                    *RestConfigResultStats*
+
+    Fields: ~
+        {enable}  (boolean)    Whether enable statistics or not
+        {stats}   (string[]|)
+
+
+RestConfigResultFormatters                          *RestConfigResultFormatters*
+
+    Fields: ~
+        {json}  (string|fun(body:string):string,table)  JSON formatter
+        {html}  (string|fun(body:string):string,table)  HTML formatter
+
+
+RestConfigHighlight                                        *RestConfigHighlight*
+
+    Fields: ~
+        {enable}   (boolean)  Whether current request highlighting is enabled or not
+        {timeout}  (number)   Duration time of the request highlighting in milliseconds
+
+
+RestConfig                                                          *RestConfig*
+
+    Fields: ~
+        {client}                    (string)   The HTTP client to be used when running requests, default is `"curl"`
+        {env_file}                  (string)   Environment variables file to be used for the request variables in the document
+        {env_pattern}               (string)   Environment variables file pattern for telescope.nvim
+        {env_edit_command}          (string)   Neovim command to edit an environment file, default is `"tabedit"`
+        {encode_url}                (boolean)  Encode URL before making request
+        {skip_ssl_verification}     (boolean)  Skip SSL verification, useful for unknown certificates
+        {custom_dynamic_variables}  ()
+
+
+config.set()                                                        *config.set*
+
+
+vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/doc/rest-nvim-curl.txt b/doc/rest-nvim-curl.txt
new file mode 100644
index 00000000..230cde05
--- /dev/null
+++ b/doc/rest-nvim-curl.txt
@@ -0,0 +1,18 @@
+==============================================================================
+rest.nvim cURL client                                    *rest-nvim.client.curl*
+
+
+ rest.nvim cURL client implementation
+
+
+client.request({request})                                       *client.request*
+    Execute an HTTP request using cURL
+
+    Parameters: ~
+        {request}  (Request)  Request data to be passed to cURL
+
+    Returns: ~
+        (table)  request information (url, method, headers, body, etc)
+
+
+vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/doc/rest-nvim-parser.txt b/doc/rest-nvim-parser.txt
new file mode 100644
index 00000000..d4ec8d5e
--- /dev/null
+++ b/doc/rest-nvim-parser.txt
@@ -0,0 +1,178 @@
+==============================================================================
+rest.nvim parsing module dynamic variables       *rest-nvim.parser.dynamic_vars*
+
+
+ rest.nvim dynamic variables
+
+
+dynamic_vars.retrieve_all()                          *dynamic_vars.retrieve_all*
+    Retrieve all dynamic variables from both rest.nvim and the ones declared by
+    the user on his configuration
+    @return { [string]: fun():string }[] An array-like table of tables which contains dynamic variables definition
+
+
+dynamic_vars.read({name})                                    *dynamic_vars.read*
+    Look for a dynamic variable and evaluate it
+
+    Parameters: ~
+        {name}  (string)  The dynamic variable name
+
+    Returns: ~
+        (string|nil)  dynamic variable value or `nil` if the dynamic variable was not found
+
+
+==============================================================================
+rest.nvim parsing module environment variables       *rest-nvim.parser.env_vars*
+
+
+ rest.nvim environment variables
+
+
+env_vars.set_var({name}, {value})                             *env_vars.set_var*
+    Set an environment variable for the current Neovim session
+
+    Parameters: ~
+        {name}   (string)                 Variable name
+        {value}  (string|number|boolean)  Variable value
+
+    See: ~
+        |vim.env|
+
+
+env_vars.read_file({quiet})                                 *env_vars.read_file*
+    Read the environment variables file from the rest.nvim configuration
+    and store all the environment variables in the `vim.env` metatable
+
+    Parameters: ~
+        {quiet}  (boolean)  Whether to fail silently if an environment file is not found, defaults to `false`
+
+    See: ~
+        |vim.env|
+
+
+==============================================================================
+rest.nvim parsing module script variables         *rest-nvim.parser.script_vars*
+
+
+ rest.nvim script variables
+
+
+script_vars.load({script_str}, {res})                         *script_vars.load*
+    Load a script_variable content and evaluate it
+
+    Parameters: ~
+        {script_str}  (string)  The script variable content
+        {res}         (table)   Request response body
+
+
+==============================================================================
+rest.nvim tree-sitter parsing module                          *rest-nvim.parser*
+
+
+Parsing module with tree-sitter, we use tree-sitter there to extract
+all the document nodes and their content from the HTTP files, then we
+start doing some other internal parsing like variables expansion and so on
+
+
+NodesList                                                            *NodesList*
+
+    Type: ~
+        
+
+
+Variables                                                            *Variables*
+
+    Type: ~
+        
+
+
+parser.get_node_at_cursor()                          *parser.get_node_at_cursor*
+
+    Returns: ~
+        (string|nil)  type
+
+
+                                                      *parser.look_behind_until*
+parser.look_behind_until({node}, {query})
+    Recursively look behind `node` until `query` node type is found
+
+    Parameters: ~
+        {node}   (TSNode|nil)  Tree-sitter node, defaults to the node at the cursor position if not passed
+        {query}  (string)      The tree-sitter node type that we are looking for
+
+    Returns: ~
+        (TSNode|nil)
+
+
+                                                          *parser.parse_request*
+parser.parse_request({children_nodes}, {variables})
+    Parse a request tree-sitter node
+
+    Parameters: ~
+        {children_nodes}  (NodesList)  Tree-sitter nodes
+        {variables}       (Variables)  HTTP document variables list
+
+    Returns: ~
+        (table)  table containing the request target `url` and `method` to be used
+
+
+                                                          *parser.parse_headers*
+parser.parse_headers({header_nodes}, {variables})
+    Parse request headers tree-sitter nodes
+
+    Parameters: ~
+        {header_nodes}  (NodesList)  Tree-sitter nodes
+        {variables}     (Variables)  HTTP document variables list
+
+    Returns: ~
+        (table)  table containing the headers in a key-value style
+
+
+                                                             *parser.parse_body*
+parser.parse_body({children_nodes}, {variables})
+    Parse a request tree-sitter node body
+
+    Parameters: ~
+        {children_nodes}  (NodesList)  Tree-sitter nodes
+        {variables}       (Variables)  HTTP document variables list
+
+    Returns: ~
+        (table)  body table
+
+
+parser.parse_script({req_node})                            *parser.parse_script*
+    Get a script variable node and return its content
+
+    Parameters: ~
+        {req_node}  (TSNode)  Tree-sitter request node
+
+    Returns: ~
+        (string)  variables content
+
+
+RequestReq                                                          *RequestReq*
+
+    Fields: ~
+        {method}         (string)  The request method
+        {url}            (string)  The request URL
+        {http_version?}  (string)  The request HTTP protocol
+
+
+Request                                                                *Request*
+
+    Fields: ~
+        {request}  (RequestReq)
+        {headers}  ()
+
+
+parser.parse({req_node})                                          *parser.parse*
+    Parse a request and return the request on itself, its headers and body
+
+    Parameters: ~
+        {req_node}  (TSNode)  Tree-sitter request node
+
+    Returns: ~
+        (Request)  containing the request data
+
+
+vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/doc/rest-nvim.txt b/doc/rest-nvim.txt
index f5eda774..db41d669 100644
--- a/doc/rest-nvim.txt
+++ b/doc/rest-nvim.txt
@@ -1,11 +1,4 @@
-*rest-nvim.txt* A fast Neovim http client written in Lua based on curl
-
-             ______                                _        ~
-            (_____ \              _               (_)       ~
-             _____) )_____  ___ _| |_   ____ _   _ _ ____   ~
-            |  __  /| ___ |/___|_   _) |  _ \ | | | |    \  ~
-            | |  \ \| ____|___ | | |_ _| | | \ V /| | | | | ~
-            |_|   |_|_____|___/   \__|_)_| |_|\_/ |_|_|_|_| ~
+*rest-nvim.txt* A very fast, powerful, extensible and asynchronous Neovim HTTP client.
 
 
  NTBBloodbath                                                      *rest-nvim*
@@ -21,9 +14,8 @@ CONTENTS                                                   *rest-nvim-contents*
         3. Import body from external file......|rest-nvim-usage-external-files|
         4. Environment Variables........|rest-nvim-usage-environment-variables|
         5. Dynamic Variables................|rest-nvim-usage-dynamic-variables|
-        6. Callbacks................................|rest-nvim-usage-callbacks|
-        7. Pre-script..............................|rest-nvim-usage-pre-script|
-    5. Known issues..........................................|rest-nvim-issues|
+        6. Script Variables..................|rest-nvim-usage-script-variables|
+        7. Hooks........................................|rest-nvim-usage-hooks|
     6. License..............................................|rest-nvim-license|
     7. Contributing....................................|rest-nvim-contributing|
 
@@ -31,57 +23,118 @@ CONTENTS                                                   *rest-nvim-contents*
 ===============================================================================
 INTRODUCTION                                                  *rest-nvim-intro*
 
-`rest.nvim` is a fast Neovim http client written in Lua which makes use of a
-curl wrapper made in pure Lua by github.com/tami5 and implemented in the
-plugin `plenary.nvim` so, in other words, `rest.nvim` is a curl wrapper so you
-don't have to leave Neovim!
-
+`rest.nvim` by default makes use of native `cURL` bindings. In this way, you
+get absolutely all the power that cURL provides from the comfort of our editor
+just by using a keybind and without wasting the precious resources of your
+machine.
 
 ===============================================================================
 FEATURES                                                   *rest-nvim-features*
 
 - Easy to use
-- Fast execution time
-- Run request under cursor
-- Syntax highlight for http files and output
-- Possibility of using environment variables in http files
-- Set environment variables based on the response
+- Friendly and organized request results window
+- Fast runtime with statistics about your request
+- Set custom pre-request and post-request hooks to dynamically interact with the data
+- Easily set environment variables based on the response to re-use the data later
+- Tree-sitter based parsing and syntax highlighting for speed and perfect accuracy
+- Possibility of using dynamic/environment variables and Lua scripting in HTTP files
 
 
 ===============================================================================
 QUICK START                                             *rest-nvim-quick-start*
 
 After installing `rest.nvim` you will need to configure it using a `setup`
-function, it looks like this by default: >lua
-
-    require("rest-nvim").setup({
-      -- Open request results in a horizontal split
-      result_split_horizontal = false,
-     -- Keep the http file buffer above|left when split horizontal|vertical
-     result_split_in_place = false,
-      -- Skip SSL verification, useful for unknown certificates
-      skip_ssl_verification = false,
-      -- Highlight request on run
-      highlight = {
-	  enabled = true,
-	  timeout = 150,
+function, it looks like this by default:
+>lua
+  require("rest-nvim").setup({
+    client = "curl",
+    env_file = ".env",
+    env_pattern = "\\.env$",
+    env_edit_command = "tabedit",
+    encode_url = true,
+    skip_ssl_verification = false,
+    custom_dynamic_variables = {},
+    logs = {
+      level = "info",
+      save = true,
+    },
+    result = {
+      split = {
+        horizontal = false,
+        in_place = false,
+        stay_in_current_window_after_split = true,
       },
-      -- Jump to request line on run
-      jump_to_request = false,
-      env_file = '.env',
-      yank_dry_run = true,
-    })
+      behavior = {
+        decode_url = true,
+        show_info = {
+          url = true,
+          headers = true,
+          http_info = true,
+          curl_command = true,
+        },
+        statistics = {
+          enable = true,
+          ---@see https://curl.se/libcurl/c/curl_easy_getinfo.html
+          stats = {
+            { "total_time", title = "Time taken:" },
+            { "size_download_t", title = "Download size:" },
+          },
+        },
+        formatters = {
+          json = "jq",
+          html = function(body)
+            if vim.fn.executable("tidy") == 0 then
+              return body, { found = false, name = "tidy" }
+            end
+            local fmt_body = vim.fn.system({
+              "tidy",
+              "-i",
+              "-q",
+              "--tidy-mark",      "no",
+              "--show-body-only", "auto",
+              "--show-errors",    "0",
+              "--show-warnings",  "0",
+              "-",
+            }, body):gsub("\n$", "")
+
+            return fmt_body, { found = true, name = "tidy" }
+          end,
+        },
+      },
+    },
+    highlight = {
+      enable = true,
+      timeout = 750,
+    },
+    ---Example:
+    ---
+    ---```lua
+    ---keybinds = {
+    ---  {
+    ---    "<localleader>rr", ":Rest run", "Run request under the cursor",
+    ---  },
+    ---  {
+    ---    "<localleader>rl", ":Rest run last", "Re-run latest request",
+    ---  },
+    ---}
+    ---
+    ---```
+    ---@see vim.keymap.set
+    keybinds = {},
+  })
+<
+Please refer to |rest-nvim.config| for more information and documentation.
 
 In this section we will be using `https://reqres.in/` for requests.
 
 Let's say we want to create a new user and send our body as a JSON, so we
 will do the following:
 
-    1. We declare the HTTP method to use followed by the URL. >lua
+    1. We declare the HTTP method to use followed by the URL. >http
        POST https://reqres.in/api/users
 <
     2. Since we want to send our body as a JSON object, we set the
-       Content-Type header. >http
+       Content-Type header. >
        Content-Type: application/json
 <
     3. Now, we set the body of our request. >json
@@ -90,43 +143,57 @@ will do the following:
            "job": "leader"
        }
 <
-    4. Finally, we place the cursor over or below the method of our request 
-       and call `rest.nvim` with `:lua require('rest-nvim').run()`.
-
-Since the way to call rest.nvim with Lua is not comfortable, rest.nvim
-exposes a command to be mapped. See |rest-nvim-usage-commands|
+    4. Finally, we place the cursor over or below the method of our request
+       and call `rest.nvim` with the following command: >vim
+       :Rest run
+<
+To get a better understanding of the `:Rest` command, please see |rest-nvim.commands|
 
 
 ===============================================================================
 USAGE                                                         *rest-nvim-usage*
 
-Create a new http file or open an existing one and place the cursor over the
-request line (e.g. `GET http://localhost:3000/foo`) or below and run `rest.nvim`
-(see |rest-nvim-usage-commands|).
+Create a new HTTP file or open an existing one and place the cursor over the
+request and run the `:Rest run` command (see |rest-nvim.commands|).
 
 Notes:
-    - `rest.nvim` follows the RFC 2616 request format so any other http file
-      should work without problems.
-    - `rest.nvim` supports multiple http requests in one file. It selects the
-      nearest request in or above the current cursor line.
+    - You can find examples of use in the `tests` directory in the GitHub
+      repository.
+    - `rest.nvim` supports multiple HTTP requests in one file. It selects the
+      request in the current cursor line, no matters the position as long as
+      the cursor is on a request tree-sitter node.
 
 
 ===============================================================================
 COMMANDS                                             *rest-nvim-usage-commands*
 
-- `<Plug>RestNvim`
-  Run `rest.nvim` in the current cursor position.
+`rest.nvim` exposes a `:Rest` command that is available only in HTTP buffers,
+this command has very useful subcommands for you (see |rest-nvim.commands|).
+Some of these commands are the following:
+
+- `:Rest run`
+  Execute one or several HTTP requests depending on the given `scope`.
+  Defaults to the request under the cursor (`:Rest run cursor`).
+
+- `:Rest last`
+  Re-run the last executed request, alias to `:Rest run last`.
+
+- `:Rest env`
+  Manage the environment file that is currently in use while running requests.
+
+If you have used `rest.nvim` before v2 (aka `Thunder Rest`), you will know that
+before we used `<Plug>` commands that had to be used in keybinds. This has
+changed, and in order not to completely break your workflow, these
+commands (`<Plug>RestNvim` and `<Plug>RestNvimLast`) will still work until
+the next version, but it is highly recommended to update your setup.
+
+Unfortunately, the following command has had to be removed as it cannot be
+re-implemented with the `rest.nvim` v2 architecture and workflow:
 
 - `<Plug>RestNvimPreview`
   Same as `RestNvim` but it returns the cURL command without executing the
   request. Intended for debugging purposes.
 
-- `:RestLog`
-  Shows `rest.nvim` logs (export DEBUG_PLENARY=debug for more logs).
-
-- `:RestSelectEnv path/to/env`
-  Set the path to an env file.
-
 
 ===============================================================================
 REQUESTS                                             *rest-nvim-usage-requests*
@@ -145,8 +212,12 @@ IMPORT BODY FROM EXTERNAL FILE                 *rest-nvim-usage-external-files*
 
 `rest.nvim` allows the http file to import the body from an external file.
 
-The syntax is `< path/to/file.json`. `rest.nvim` supports absolute and relative
-paths to the external file.
+The syntax is to append an external body from a file is `< path/to/file.json`.
+
+You can also choose a name for the file sent to the backend using
+the syntax `<@user.json path/to/file.json`.
+
+`rest.nvim` supports absolute and relative paths to the external file.
 
 
 ===============================================================================
@@ -154,62 +225,66 @@ ENVIRONMENT VARIABLES                   *rest-nvim-usage-environment-variables*
 
 `rest.nvim` allows the use of environment variables in requests.
 
-To use environment variables, the following syntax is used: `{{VARIABLE_NAME}}`
+To use environment variables, the following syntax is used: `{{VARIABLE_NAME}}`.
 
 These environment variables can be obtained from:
-    - File in the current working directory (env_file in config or '.env')
-    - System
+    - Your current Neovim session (`vim.env`)
+    - Your system shell environment.
+    - File in the current working directory (`env_file` in config, `.env` by default).
 
-Environment variables can be set in .env format or in json.
+Environment variables can be set in `.env` format or in `json`.
 
-To change the environment for the session use :RestSelectEnv path/to/environment
+To change the environment for the session use `:Rest env set path/to/environment`
 
-Environment variables can be set dynamically from the response body. 
-(see rest-nvim-usage-dynamic-variables)
+Environment variables can be set dynamically from the response body.
+(see |rest-nvim-usage-script-variables|)
 
 
 ===============================================================================
-RESPONSE SCRIPT                                     *rest-nvim-response-script*
+SCRIPT VARIABLES                                   *rest-nvim-script-variables*
 
-A lua script can be run after a request has completed. This script must below
-the body and wrapped in {% script %}. A context table is avaliable in the 
+A Lua script can be run after a request has completed. This script must below
+the body and wrapped in `--{% script --%}`. A context table is avaliable in the
 response script. The context table can be used to read the response and set
-environment variables. 
+environment variables.
 
-The context table: >lua
+The context table:
+>lua
     {
       result = res,
-      pretty_print = vim.pretty_print,
-      json_decode = vim.fn.json_decode,
+      pretty_print = vim.print,
+      json_decode = vim.json.decode,
       set_env = utils.set_env,
     }
 <
 Now environment variables can be set like so:
->
+>http
     GET https://jsonplaceholder.typicode.com/posts/3
-     
-    {% 
-     
+
+    --{%
+
     local body = context.json_decode(context.result.body)
     context.set_env("postId", body.id)
-     
-    %}
+
+    --%}
 <
+
+
 ===============================================================================
 DYNAMIC VARIABLES                           *rest-nvim-usage-dynamic-variables*
 
 `rest.nvim` allows the use of dynamic variables in requests.
 
 The following dynamic variables are currently supported:
-    - $uuid: generates a universally unique identifier (UUID-v4)
-    - $timestamp: generates the current UNIX timestamp (seconds since epoch)
-    - $randomInt: generates a random integer between 0 and 1000
+    - `$uuid`: generates a universally unique identifier (UUID-v4)
+    - `$timestamp`: generates the current UNIX timestamp (seconds since epoch)
+    - `$randomInt`: generates a random integer between 0 and 1000
 
-To use dynamic variables, the following syntax is used: `{{DYNAMIC_VARIABLE}}`, 
+To use dynamic variables, the following syntax is used: `{{$DYNAMIC_VARIABLE}}`,
 e.g. `{{$uuid}}`
 
-You can extend or overwrite built-in dynamic variables, with the config key >lua
-
+You can extend or overwrite built-in dynamic variables, with the config key
+>lua
     -- custom_dynamic_variables:
     require("rest-nvim").setup({
       custom_dynamic_variables = {
@@ -224,78 +299,38 @@ You can extend or overwrite built-in dynamic variables, with the config key >lua
         end,
       },
     })
+<
 
-===============================================================================
-CALLBACKS                                             *rest-nvim-usage-callbacks*
-
-rest.nvim fires different events upon requests:
-   - a User RestStartRequest event when launching the request
-   - a User RestStopRequest event when the requests finishes or errors out >lua
 
-    vim.api.nvim_create_autocmd("User", {
-    pattern = "RestStartRequest",
-    once = true,
-      callback = function(opts)
-	 print("IT STARTED")
-	 vim.pretty_print(opts)
-      end,
-    })
-<
 ===============================================================================
-Pre-Script                                           *rest-nvim-usage-pre-script*
+HOOKS                                                   *rest-nvim-usage-hooks*
 
-rest.nvim allows configuring a pre-script that is invoked before launching a
-request. This script can be used to dynamically modify the request headers and
-body. The script receives the request opts and environment variables: >lua
+`rest.nvim` fires different events upon requests:
+   - a User `RestStartRequest` event when launching the request.
+   - a User `RestStopRequest` event when the requests finishes.
 
-    -- pre-script example:
-    require("rest-nvim").setup({
-      request = {
-	pre_script = function(opts, variables)
-	  -- Access request body
-	  local body = opts["body"]
-	  -- Access request headers
-	  local content_type = opts["headers"]["Content-Type"]
-	  -- Access environment variables
-	  local secret_key = variables["SECRET_KEY"]
-
-	  -- Set custom request headers.
-	  opts["headers"]["X-Signature"] = signature(body, secret_key)
-	  opts["headers"]["X-Timestamp"] = os.time
-	end
-      },
-    })
+>lua
+  vim.api.nvim_create_autocmd("User", {
+    pattern = "RestStartRequest",
+    once = true, -- This is optional, only if you want the hook to run once
+    callback = function()
+      print("Started request")
+      -- You can access and modify the request data (body, headers, etc) by
+      -- using the following temporal global variable
+      vim.print(_G._rest_nvim_req_data)
+      -- You can also access environment variables from both your current
+      -- shell session and your environment file by using 'vim.env'
+      _G._rest_nvim_req_data.headers["USER"] = vim.env.USERNAME
+    end,
+  })
 <
-===============================================================================
-KNOWN ISSUES                                                 *rest-nvim-issues*
-
-    - Nothing here at the moment :)
 
 
 ===============================================================================
 LICENSE                                                     *rest-nvim-license*
 
-rest.nvim is distributed under MIT License.
-
-Copyright (c) 2021 NTBBloodbath
-
-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.
+`rest.nvim` is distributed under GPLv3 License, please read the LICENSE file
+in the GitHub repository (`rest-nvim/rest.nvim`).
 
 
 ===============================================================================
@@ -307,4 +342,5 @@ CONTRIBUTING                                           *rest-nvim-contributing*
     4. Push to the branch (`git push origin my-new-feature`)
     5. Create a new Pull Request
 
+
 vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/doc/tags b/doc/tags
index e1967e62..f12ab59a 100644
--- a/doc/tags
+++ b/doc/tags
@@ -1,17 +1,94 @@
+Logger	rest-nvim-api.txt	/*Logger*
+LoggerConfig	rest-nvim-api.txt	/*LoggerConfig*
+LoggerLevels	rest-nvim-api.txt	/*LoggerLevels*
+NodesList	rest-nvim-parser.txt	/*NodesList*
+Request	rest-nvim-parser.txt	/*Request*
+RequestReq	rest-nvim-parser.txt	/*RequestReq*
+RestCmd	rest-nvim-commands.txt	/*RestCmd*
+RestConfig	rest-nvim-config.txt	/*RestConfig*
+RestConfigDebug	rest-nvim-config.txt	/*RestConfigDebug*
+RestConfigHighlight	rest-nvim-config.txt	/*RestConfigHighlight*
+RestConfigLogs	rest-nvim-config.txt	/*RestConfigLogs*
+RestConfigResult	rest-nvim-config.txt	/*RestConfigResult*
+RestConfigResultBehavior	rest-nvim-config.txt	/*RestConfigResultBehavior*
+RestConfigResultFormatters	rest-nvim-config.txt	/*RestConfigResultFormatters*
+RestConfigResultInfo	rest-nvim-config.txt	/*RestConfigResultInfo*
+RestConfigResultSplit	rest-nvim-config.txt	/*RestConfigResultSplit*
+RestConfigResultStats	rest-nvim-config.txt	/*RestConfigResultStats*
+ResultPane	rest-nvim-api.txt	/*ResultPane*
+Variables	rest-nvim-parser.txt	/*Variables*
+api.VERSION	rest-nvim-api.txt	/*api.VERSION*
+api.namespace	rest-nvim-api.txt	/*api.namespace*
+api.register_rest_autocmd	rest-nvim-api.txt	/*api.register_rest_autocmd*
+api.register_rest_keybind	rest-nvim-api.txt	/*api.register_rest_keybind*
+api.register_rest_subcommand	rest-nvim-api.txt	/*api.register_rest_subcommand*
+client.request	rest-nvim-curl.txt	/*client.request*
+config.set	rest-nvim-config.txt	/*config.set*
+dynamic_vars.read	rest-nvim-parser.txt	/*dynamic_vars.read*
+dynamic_vars.retrieve_all	rest-nvim-parser.txt	/*dynamic_vars.retrieve_all*
+env_vars.read_file	rest-nvim-parser.txt	/*env_vars.read_file*
+env_vars.set_var	rest-nvim-parser.txt	/*env_vars.set_var*
+functions.cycle_result_pane	rest-nvim-api.txt	/*functions.cycle_result_pane*
+functions.env	rest-nvim-api.txt	/*functions.env*
+functions.exec	rest-nvim-api.txt	/*functions.exec*
+functions.find_env_files	rest-nvim-api.txt	/*functions.find_env_files*
+help.close	rest-nvim-api.txt	/*help.close*
+help.open	rest-nvim-api.txt	/*help.open*
+logger:debug	rest-nvim-api.txt	/*logger:debug*
+logger:error	rest-nvim-api.txt	/*logger:error*
+logger:info	rest-nvim-api.txt	/*logger:info*
+logger:new	rest-nvim-api.txt	/*logger:new*
+logger:set_log_level	rest-nvim-api.txt	/*logger:set_log_level*
+logger:trace	rest-nvim-api.txt	/*logger:trace*
+logger:warn	rest-nvim-api.txt	/*logger:warn*
+parser.get_node_at_cursor	rest-nvim-parser.txt	/*parser.get_node_at_cursor*
+parser.look_behind_until	rest-nvim-parser.txt	/*parser.look_behind_until*
+parser.parse	rest-nvim-parser.txt	/*parser.parse*
+parser.parse_body	rest-nvim-parser.txt	/*parser.parse_body*
+parser.parse_headers	rest-nvim-parser.txt	/*parser.parse_headers*
+parser.parse_request	rest-nvim-parser.txt	/*parser.parse_request*
+parser.parse_script	rest-nvim-parser.txt	/*parser.parse_script*
 rest-nvim	rest-nvim.txt	/*rest-nvim*
 rest-nvim-contents	rest-nvim.txt	/*rest-nvim-contents*
 rest-nvim-contributing	rest-nvim.txt	/*rest-nvim-contributing*
 rest-nvim-features	rest-nvim.txt	/*rest-nvim-features*
 rest-nvim-intro	rest-nvim.txt	/*rest-nvim-intro*
-rest-nvim-issues	rest-nvim.txt	/*rest-nvim-issues*
 rest-nvim-license	rest-nvim.txt	/*rest-nvim-license*
 rest-nvim-quick-start	rest-nvim.txt	/*rest-nvim-quick-start*
-rest-nvim-response-script	rest-nvim.txt	/*rest-nvim-response-script*
+rest-nvim-script-variables	rest-nvim.txt	/*rest-nvim-script-variables*
 rest-nvim-usage	rest-nvim.txt	/*rest-nvim-usage*
-rest-nvim-usage-callbacks	rest-nvim.txt	/*rest-nvim-usage-callbacks*
 rest-nvim-usage-commands	rest-nvim.txt	/*rest-nvim-usage-commands*
 rest-nvim-usage-dynamic-variables	rest-nvim.txt	/*rest-nvim-usage-dynamic-variables*
 rest-nvim-usage-environment-variables	rest-nvim.txt	/*rest-nvim-usage-environment-variables*
 rest-nvim-usage-external-files	rest-nvim.txt	/*rest-nvim-usage-external-files*
+rest-nvim-usage-hooks	rest-nvim.txt	/*rest-nvim-usage-hooks*
 rest-nvim-usage-requests	rest-nvim.txt	/*rest-nvim-usage-requests*
+rest-nvim.api	rest-nvim-api.txt	/*rest-nvim.api*
+rest-nvim.client.curl	rest-nvim-curl.txt	/*rest-nvim.client.curl*
+rest-nvim.commands	rest-nvim-commands.txt	/*rest-nvim.commands*
+rest-nvim.config	rest-nvim-config.txt	/*rest-nvim.config*
+rest-nvim.functions	rest-nvim-api.txt	/*rest-nvim.functions*
+rest-nvim.logger	rest-nvim-api.txt	/*rest-nvim.logger*
+rest-nvim.parser	rest-nvim-parser.txt	/*rest-nvim.parser*
+rest-nvim.parser.dynamic_vars	rest-nvim-parser.txt	/*rest-nvim.parser.dynamic_vars*
+rest-nvim.parser.env_vars	rest-nvim-parser.txt	/*rest-nvim.parser.env_vars*
+rest-nvim.parser.script_vars	rest-nvim-parser.txt	/*rest-nvim.parser.script_vars*
+rest-nvim.result	rest-nvim-api.txt	/*rest-nvim.result*
+rest-nvim.result.help	rest-nvim-api.txt	/*rest-nvim.result.help*
+rest-nvim.result.winbar	rest-nvim-api.txt	/*rest-nvim.result.winbar*
 rest-nvim.txt	rest-nvim.txt	/*rest-nvim.txt*
+rest-nvim.utils	rest-nvim-api.txt	/*rest-nvim.utils*
+result.bufnr	rest-nvim-api.txt	/*result.bufnr*
+result.display_buf	rest-nvim-api.txt	/*result.display_buf*
+result.get_or_create_buf	rest-nvim-api.txt	/*result.get_or_create_buf*
+result.write_block	rest-nvim-api.txt	/*result.write_block*
+result.write_res	rest-nvim-api.txt	/*result.write_res*
+script_vars.load	rest-nvim-parser.txt	/*script_vars.load*
+utils.escape	rest-nvim-api.txt	/*utils.escape*
+utils.file_exists	rest-nvim-api.txt	/*utils.file_exists*
+utils.highlight	rest-nvim-api.txt	/*utils.highlight*
+utils.read_file	rest-nvim-api.txt	/*utils.read_file*
+winbar.current_pane_index	rest-nvim-api.txt	/*winbar.current_pane_index*
+winbar.get_content	rest-nvim-api.txt	/*winbar.get_content*
+winbar.set_hl	rest-nvim-api.txt	/*winbar.set_hl*
+winbar.set_pane	rest-nvim-api.txt	/*winbar.set_pane*
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index 11ae34c1..00000000
--- a/flake.lock
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "nodes": {
-    "flake-utils": {
-      "locked": {
-        "lastModified": 1667395993,
-        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
-        "type": "github"
-      },
-      "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "type": "github"
-      }
-    },
-    "nixpkgs": {
-      "locked": {
-        "lastModified": 1670751203,
-        "narHash": "sha256-XdoH1v3shKDGlrwjgrNX/EN8s3c+kQV7xY6cLCE8vcI=",
-        "owner": "nixos",
-        "repo": "nixpkgs",
-        "rev": "64e0bf055f9d25928c31fb12924e59ff8ce71e60",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nixos",
-        "ref": "nixos-unstable",
-        "repo": "nixpkgs",
-        "type": "github"
-      }
-    },
-    "root": {
-      "inputs": {
-        "flake-utils": "flake-utils",
-        "nixpkgs": "nixpkgs"
-      }
-    }
-  },
-  "root": "root",
-  "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index 84538380..00000000
--- a/flake.nix
+++ /dev/null
@@ -1,84 +0,0 @@
-{
-  description = "rest.nvim: A fast Neovim http client written in Lua";
-
-  inputs = {
-    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
-    flake-utils.url = "github:numtide/flake-utils";
-  };
-
-  outputs = { self, nixpkgs, flake-utils, ... }:
-    flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
-      let
-        pkgs = nixpkgs.legacyPackages.${system};
-
-        mkDevShell = luaVersion:
-          let
-            luaEnv = pkgs."lua${luaVersion}".withPackages (lp: with lp; [
-              busted
-              luacheck
-              luarocks
-            ]);
-          in
-          pkgs.mkShell {
-            name = "rest-nvim";
-            buildInputs = [
-              pkgs.sumneko-lua-language-server
-              luaEnv
-              pkgs.stylua
-            ];
-
-            shellHook = let 
-              myVimPackage = with pkgs.vimPlugins; {
-                      start = [ plenary-nvim (nvim-treesitter.withPlugins (
-                      plugins: with plugins; [
-                        tree-sitter-lua
-                        tree-sitter-http
-                        tree-sitter-json
-                      ]
-                    ))];
-                };
-              packDirArgs.myNeovimPackages = myVimPackage;
-            in 
-              ''
-              export DEBUG_PLENARY="debug"
-              cat <<-EOF > minimal.vim
-                set rtp+=.
-                set packpath^=${pkgs.vimUtils.packDir packDirArgs}
-              EOF
-              '';
-          };
-
-      in
-      {
-
-        devShells = {
-          default = self.devShells.${system}.luajit;
-          ci = let 
-            neovimConfig = pkgs.neovimUtils.makeNeovimConfig {
-              plugins = with pkgs.vimPlugins; [
-                { plugin = (nvim-treesitter.withPlugins (
-                    plugins: with plugins; [
-                      tree-sitter-lua
-                      tree-sitter-http
-                      tree-sitter-json
-                    ]
-                  ));
-                }
-                { plugin = plenary-nvim; }
-              ];
-              customRC = "";
-              wrapRc = false;
-            };
-            myNeovim = pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped neovimConfig;
-            in 
-              (mkDevShell "jit").overrideAttrs(oa: {
-                buildInputs = oa.buildInputs ++ [ myNeovim ];
-              });
-
-          luajit = mkDevShell "jit";
-          lua-51 = mkDevShell "5_1";
-          lua-52 = mkDevShell "5_2";
-        };
-      });
-}
-
diff --git a/ftdetect/http.lua b/ftdetect/http.lua
new file mode 100644
index 00000000..4cd204f0
--- /dev/null
+++ b/ftdetect/http.lua
@@ -0,0 +1,5 @@
+vim.filetype.add({
+  extension = {
+    http = "http",
+  },
+})
diff --git a/ftdetect/http.vim b/ftdetect/http.vim
deleted file mode 100644
index 02b25691..00000000
--- a/ftdetect/http.vim
+++ /dev/null
@@ -1 +0,0 @@
-au BufRead,BufNewFile *.http set ft=http
diff --git a/ftplugin/http.lua b/ftplugin/http.lua
new file mode 100644
index 00000000..ea55aa57
--- /dev/null
+++ b/ftplugin/http.lua
@@ -0,0 +1 @@
+vim.bo.commentstring = "# %s"
diff --git a/ftplugin/http.vim b/ftplugin/http.vim
deleted file mode 100644
index 9c2e80a0..00000000
--- a/ftplugin/http.vim
+++ /dev/null
@@ -1 +0,0 @@
-set commentstring=#\ %s
diff --git a/lua/lualine/components/rest.lua b/lua/lualine/components/rest.lua
index 934e7c04..0513668e 100644
--- a/lua/lualine/components/rest.lua
+++ b/lua/lualine/components/rest.lua
@@ -1,6 +1,5 @@
 local lualine_require = require("lualine_require")
 local M = lualine_require.require("lualine.component"):extend()
-local config = require("rest-nvim.config")
 
 local default_options = {
   fg = "#428890",
@@ -23,7 +22,7 @@ end
 function M.update_status()
   local current_filetype = vim.bo.filetype
   if current_filetype == "http" then
-    return config.get("env_file")
+    return _G._rest_nvim.env_file
   end
   return ""
 end
diff --git a/lua/rest-nvim/api.lua b/lua/rest-nvim/api.lua
new file mode 100644
index 00000000..5638d112
--- /dev/null
+++ b/lua/rest-nvim/api.lua
@@ -0,0 +1,79 @@
+---@mod rest-nvim.api rest.nvim Lua API
+---
+---@brief [[
+---
+---The Lua API for rest.nvim
+---Intended for use by third-party modules that extend its functionalities.
+---
+---@brief ]]
+
+local api = {}
+
+local keybinds = require("rest-nvim.keybinds")
+local autocmds = require("rest-nvim.autocmds")
+local commands = require("rest-nvim.commands")
+
+---rest.nvim API version, equals to the current rest.nvim version. Meant to be used by modules later
+---@type string
+---@see vim.version
+api.VERSION = "2.0.0"
+
+---rest.nvim namespace used for buffer highlights
+---@type number
+---@see vim.api.nvim_create_namespace
+api.namespace = vim.api.nvim_create_namespace("rest-nvim")
+
+---Register a new autocommand in the `Rest` augroup
+---@see vim.api.nvim_create_augroup
+---@see vim.api.nvim_create_autocmd
+---
+---@param events string[] Autocommand events, see `:h events`
+---@param cb string|fun(args: table) Autocommand lua callback, runs a Vimscript command instead if it is a `string`
+---@param description string Autocommand description
+function api.register_rest_autocmd(events, cb, description)
+  autocmds.register_autocmd(events, cb, description)
+end
+
+---Register a new `:Rest` subcommand
+---@param name string The name of the subcommand to register
+---@param cmd RestCmd
+function api.register_rest_subcommand(name, cmd)
+  commands.register_subcommand(name, cmd)
+end
+
+---Register a new keybinding
+---@see vim.keymap.set
+---
+---@param mode string Keybind mode
+---@param lhs string Keybind trigger
+---@param cmd string Command to be run
+---@param opts table Keybind options
+function api.register_rest_keybind(mode, lhs, cmd, opts)
+  keybinds.register_keybind(mode, lhs, cmd, opts)
+end
+
+---Execute all the pre-request hooks, functions that are meant to run before executing a request
+---
+---This function is called automatically during the execution of the requests, invoking it again could cause inconveniences
+---@see vim.api.nvim_exec_autocmds
+---@package
+function api.exec_pre_request_hooks()
+  vim.api.nvim_exec_autocmds("User", {
+    pattern = "RestStartRequest",
+    modeline = false,
+  })
+end
+
+---Execute all the post-request hooks, functions that are meant to run after executing a request
+---
+---This function is called automatically during the execution of the requests, invoking it again could cause inconveniences
+---@see vim.api.nvim_exec_autocmds
+---@package
+function api.exec_post_request_hooks()
+  vim.api.nvim_exec_autocmds("User", {
+    pattern = "RestStopRequest",
+    modeline = false,
+  })
+end
+
+return api
diff --git a/lua/rest-nvim/autocmds.lua b/lua/rest-nvim/autocmds.lua
new file mode 100644
index 00000000..5a6751f1
--- /dev/null
+++ b/lua/rest-nvim/autocmds.lua
@@ -0,0 +1,93 @@
+---@mod rest-nvim.autocmds rest.nvim autocommands
+---
+---@brief [[
+---
+--- rest.nvim autocommands
+---
+---@brief ]]
+
+local autocmds = {}
+
+local commands = require("rest-nvim.commands")
+local functions = require("rest-nvim.functions")
+local result_help = require("rest-nvim.result.help")
+
+---Set up Rest autocommands group and set `:Rest` command on `*.http` files
+function autocmds.setup()
+  local rest_nvim_augroup = vim.api.nvim_create_augroup("Rest", {})
+  vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, {
+    group = rest_nvim_augroup,
+    pattern = "*.http",
+    callback = function(args)
+      commands.init(args.buf)
+    end,
+    desc = "Set up rest.nvim commands",
+  })
+
+  vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, {
+    group = rest_nvim_augroup,
+    pattern = "rest_nvim_results",
+    callback = function(args)
+      vim.keymap.set("n", "H", function()
+        functions.cycle_result_pane("prev")
+      end, { desc = "Go to previous winbar pane" })
+      vim.keymap.set("n", "L", function()
+        functions.cycle_result_pane("next")
+      end, { desc = "Go to next winbar pane" })
+      vim.keymap.set("n", "?", result_help.open, {
+        desc = "Open rest.nvim request results help window",
+        buffer = args.buf,
+      })
+      vim.keymap.set("n", "q", function()
+        vim.api.nvim_buf_delete(args.buf, { unload = true })
+      end, { desc = "Close rest.nvim results buffer", buffer = args.buf })
+    end,
+    desc = "Set up rest.nvim results buffer keybinds",
+  })
+  vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, {
+    group = rest_nvim_augroup,
+    pattern = "rest_winbar_help",
+    callback = function(args)
+      vim.keymap.set("n", "q", result_help.close, {
+        desc = "Close rest.nvim request results help window",
+        buffer = args.buf,
+      })
+    end,
+  })
+end
+
+---Register a new autocommand in the `Rest` augroup
+---@see vim.api.nvim_create_augroup
+---@see vim.api.nvim_create_autocmd
+---
+---@param events string[] Autocommand events, see `:h events`
+---@param cb string|fun(args: table) Autocommand lua callback, runs a Vimscript command instead if it is a `string`
+---@param description string Autocommand description
+---@package
+function autocmds.register_autocmd(events, cb, description)
+  vim.validate({
+    events = { events, "table" },
+    cb = { cb, { "function", "string" } },
+    description = { description, "string" },
+  })
+
+  local autocmd_opts = {
+    group = "Rest",
+    pattern = "*.http",
+    desc = description,
+  }
+
+  if type(cb) == "function" then
+    autocmd_opts = vim.tbl_deep_extend("force", autocmd_opts, {
+      callback = cb,
+    })
+  elseif type(cb) == "string" then
+    autocmd_opts = vim.tbl_deep_extend("force", autocmd_opts, {
+      command = cb,
+    })
+  end
+
+  vim.api.nvim_create_autocmd(events, autocmd_opts)
+end
+
+return autocmds
diff --git a/lua/rest-nvim/client/curl.lua b/lua/rest-nvim/client/curl.lua
new file mode 100644
index 00000000..c48f6bd8
--- /dev/null
+++ b/lua/rest-nvim/client/curl.lua
@@ -0,0 +1,308 @@
+---@mod rest-nvim.client.curl rest.nvim cURL client
+---
+---@brief [[
+---
+--- rest.nvim cURL client implementation
+---
+---@brief ]]
+
+local client = {}
+
+local found_curl, curl = pcall(require, "cURL.safe")
+
+local utils = require("rest-nvim.utils")
+
+-- TODO: add support for running multiple requests at once for `:Rest run document`
+-- TODO: add support for submitting forms in the `client.request` function
+
+---Return the status code and the meaning of an curl error
+---see man curl for reference
+---@param code number The exit code of curl
+---@return string The curl error message
+local function curl_error(code)
+  local curl_error_dictionary = {
+    [1] = "Unsupported protocol. This build of curl has no support for this protocol.",
+    [2] = "Failed to initialize.",
+    [3] = "URL malformed. The syntax was not correct.",
+    [4] = "A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time."
+      .. "To make curl able to do this, you probably need another build of libcurl!",
+    [5] = "Couldn't resolve proxy. The given proxy host could not be resolved.",
+    [6] = "Couldn't resolve host. The given remote host was not resolved.",
+    [7] = "Failed to connect to host.",
+    [8] = "Weird server reply. The server sent data curl couldn't parse.",
+    [9] = "FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn't exist on the server.",
+    [10] = "FTP accept failed. While waiting for the server to connect back when an active FTP session is used, an error code was sent over the control connection or similar.",
+    [11] = "FTP weird PASS reply. Curl couldn't parse the reply sent to the PASS request.",
+    [12] = "During an active FTP session while waiting for the server to connect back to curl, the timeout expired.",
+    [13] = "FTP weird PASV reply, Curl couldn't parse the reply sent to the PASV request.",
+    [14] = "FTP weird 227 format. Curl couldn't parse the 227-line the server sent.",
+    [15] = "FTP can't get host. Couldn't resolve the host IP we got in the 227-line.",
+    [16] = "HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems, see the error message for details.",
+    [17] = "FTP couldn't set binary. Couldn't change transfer method to binary.",
+    [18] = "Partial file. Only a part of the file was transferred.",
+    [19] = "FTP couldn't download/access the given file, the RETR (or similar) command failed.",
+    [21] = "FTP quote error. A quote command returned error from the server.",
+    [22] = "HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used.",
+    [23] = "Write error. Curl couldn't write data to a local filesystem or similar.",
+    [25] = "FTP couldn't STOR file. The server denied the STOR operation, used for FTP uploading.",
+    [26] = "Read error. Various reading problems.",
+    [27] = "Out of memory. A memory allocation request failed.",
+    [28] = "Operation timeout. The specified time-out period was reached according to the conditions.",
+    [30] = "FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!",
+    [31] = "FTP couldn't use REST. The REST command failed. This command is used for resumed FTP transfers.",
+    [33] = 'HTTP range error. The range "command" didn\'t work.',
+    [34] = "HTTP post error. Internal post-request generation error.",
+    [35] = "SSL connect error. The SSL handshaking failed.",
+    [36] = "Bad download resume. Couldn't continue an earlier aborted download.",
+    [37] = "FILE couldn't read file. Failed to open the file. Permissions?",
+    [38] = "LDAP cannot bind. LDAP bind operation failed.",
+    [39] = "LDAP search failed.",
+    [41] = "Function not found. A required LDAP function was not found.",
+    [42] = "Aborted by callback. An application told curl to abort the operation.",
+    [43] = "Internal error. A function was called with a bad parameter.",
+    [45] = "Interface error. A specified outgoing interface could not be used.",
+    [47] = "Too many redirects. When following redirects, curl hit the maximum amount.",
+    [48] = "Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was passed on to libcurl and rejected. Read up in the manual!",
+    [49] = "Malformed telnet option.",
+    [51] = "The peer's SSL certificate or SSH MD5 fingerprint was not OK.",
+    [52] = "The server didn't reply anything, which here is considered an error.",
+    [53] = "SSL crypto engine not found.",
+    [54] = "Cannot set SSL crypto engine as default.",
+    [55] = "Failed sending network data.",
+    [56] = "Failure in receiving network data.",
+    [58] = "Problem with the local certificate.",
+    [59] = "Couldn't use specified SSL cipher.",
+    [60] = "Peer certificate cannot be authenticated with known CA certificates.",
+    [61] = "Unrecognized transfer encoding.",
+    [62] = "Invalid LDAP URL.",
+    [63] = "Maximum file size exceeded.",
+    [64] = "Requested FTP SSL level failed.",
+    [65] = "Sending the data requires a rewind that failed.",
+    [66] = "Failed to initialize SSL Engine.",
+    [67] = "The user name, password, or similar was not accepted and curl failed to log in.",
+    [68] = "File not found on TFTP server.",
+    [69] = "Permission problem on TFTP server.",
+    [70] = "Out of disk space on TFTP server.",
+    [71] = "Illegal TFTP operation.",
+    [72] = "Unknown TFTP transfer ID.",
+    [73] = "File already exists (TFTP).",
+    [74] = "No such user (TFTP).",
+    [75] = "Character conversion failed.",
+    [76] = "Character conversion functions required.",
+    [77] = "Problem with reading the SSL CA cert (path? access rights?).",
+    [78] = "The resource referenced in the URL does not exist.",
+    [79] = "An unspecified error occurred during the SSH session.",
+    [80] = "Failed to shut down the SSL connection.",
+    [82] = "Could not load CRL file, missing or wrong format (added in 7.19.0).",
+    [83] = "Issuer check failed (added in 7.19.0).",
+    [84] = "The FTP PRET command failed",
+    [85] = "RTSP: mismatch of CSeq numbers",
+    [86] = "RTSP: mismatch of Session Identifiers",
+    [87] = "unable to parse FTP file list",
+    [88] = "FTP chunk callback reported error",
+    [89] = "No connection available, the session will be queued",
+    [90] = "SSL public key does not matched pinned public key",
+    [91] = "Invalid SSL certificate status.",
+    [92] = "Stream error in HTTP/2 framing layer.",
+  }
+
+  if not curl_error_dictionary[code] then
+    return "cURL error " .. tostring(code) .. ": Unknown curl error"
+  end
+  return "cURL error " .. tostring(code) .. ": " .. curl_error_dictionary[code]
+end
+
+---Get request statistics
+---@param req table cURL request class
+---@param statistics_tbl RestConfigResultStats Statistics table
+---@return table Request statistics
+local function get_stats(req, statistics_tbl)
+  local logger = _G._rest_nvim.logger
+
+  local stats = {}
+
+  local function get_stat(req_, stat_)
+    local curl_info = curl["INFO_" .. stat_:upper()]
+    if not curl_info then
+      ---@diagnostic disable-next-line need-check-nil
+      logger:error(
+        "The cURL request stat field '"
+          .. stat_("' was not found.\nPlease take a look at: https://curl.se/libcurl/c/curl_easy_getinfo.html")
+      )
+      return
+    end
+    local stat_info = req_:getinfo(curl_info)
+
+    if stat_:find("size") then
+      stat_info = utils.transform_size(stat_info)
+    elseif stat_:find("time") then
+      stat_info = utils.transform_time(stat_info)
+    end
+    return stat_info
+  end
+
+  local stat_key, stat_title, stat_info
+  for _, stat in pairs(statistics_tbl) do
+    for k, v in pairs(stat) do
+      if type(k) == "string" and k == "title" then
+        stat_title = v
+      end
+      if type(k) == "number" then
+        stat_key = v
+        stat_info = get_stat(req, v)
+      end
+    end
+    stats[stat_key] = stat_title .. " " .. stat_info
+  end
+
+  return stats
+end
+
+---Execute an HTTP request using cURL
+---@param request Request Request data to be passed to cURL
+---@return table The request information (url, method, headers, body, etc)
+function client.request(request)
+  local ret = {}
+  local logger = _G._rest_nvim.logger
+
+  if not found_curl then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("lua-curl could not be found, therefore the cURL client will not work.")
+  else
+    -- If Host header exists then we need to tweak the request url
+    if vim.tbl_contains(vim.tbl_keys(request.headers), "Host") then
+      ---@diagnostic disable-next-line inject-field
+      request.request.url = request.headers["Host"] .. request.request.url
+      request.headers["Host"] = nil
+    elseif vim.tbl_contains(vim.tbl_keys(request.headers), "host") then
+      ---@diagnostic disable-next-line inject-field
+      request.request.url = request.headers["host"] .. request.request.url
+      request.headers["host"] = nil
+    end
+
+    -- We have to concat request headers to a single string, e.g. ["Content-Type"]: "application/json" -> "Content-Type: application/json"
+    local headers = {}
+    for name, value in pairs(request.headers) do
+      table.insert(headers, name .. ": " .. value)
+    end
+
+    -- Whether to skip SSL host and peer verification
+    local skip_ssl_verification = _G._rest_nvim.skip_ssl_verification
+    local req = curl.easy_init()
+    req:setopt({
+      url = request.request.url,
+      -- verbose = true,
+      httpheader = headers,
+      ssl_verifyhost = skip_ssl_verification,
+      ssl_verifypeer = skip_ssl_verification,
+    })
+
+    -- Encode URL query parameters and set the request URL again with the encoded values
+    local should_encode_url = _G._rest_nvim.encode_url
+    if should_encode_url then
+      -- Create a new URL as we cannot extract the URL from the req object
+      local _url = curl.url()
+      _url:set_url(request.request.url)
+      -- Re-add the request query with the encoded parameters
+      _url:set_query(_url:get_query(), curl.U_URLENCODE)
+      -- Re-add the request URL to the req object
+      req:setopt_url(_url:get_url())
+    end
+
+    -- Set request HTTP version, defaults to HTTP/1.1
+    if request.request.http_version then
+      local http_version = request.request.http_version:gsub("%.", "_")
+      req:setopt_http_version(curl["HTTP_VERSION_" .. http_version])
+    else
+      req:setopt_http_version(curl.HTTP_VERSION_1_1)
+    end
+
+    -- If the request method is not GET then we have to build the method in our own
+    -- See: https://github.com/Lua-cURL/Lua-cURLv3/issues/156
+    local method = request.request.method
+    if vim.tbl_contains({ "POST", "PUT", "PATCH", "TRACE", "OPTIONS", "DELETE" }, method) then
+      req:setopt_post(true)
+      req:setopt_customrequest(method)
+    end
+
+    -- Request body
+    --
+    -- Create a copy of the request body table to remove the unneeded `__TYPE` metadata field later
+    local body = request.body
+    if request.body.__TYPE == "json" then
+      body.__TYPE = nil
+
+      local json_body_string = vim.json.encode(body)
+      req:setopt_postfields(json_body_string)
+    elseif request.body.__TYPE == "xml" then
+      local ok, xml2lua = pcall(require, "xml2lua")
+      body.__TYPE = nil
+
+      -- Send an empty table if xml2lua is not installed
+      if ok then
+        local xml_body_string = xml2lua.toXml(body)
+        req:setopt_postfields(xml_body_string)
+      else
+        req:setopt_postfields({})
+      end
+    elseif request.body.__TYPE == "external_file" then
+      local ok, mimetypes = pcall(require, "mimetypes")
+      if ok then
+        local body_mimetype = mimetypes.guess(request.body.path)
+        local post_data = {
+          [request.body.name and request.body.name or "body"] = {
+            file = request.body.path,
+            type = body_mimetype,
+          },
+        }
+        req:post(post_data)
+      end
+    elseif request.body.__TYPE == "form_data" then
+      body.__TYPE = nil
+
+      local form = curl.form()
+      for k, v in pairs(body) do
+        form:add_content(k, v)
+      end
+      req:setopt_httppost(form)
+    end
+
+    -- Request execution
+    local res_result = {}
+    local res_headers = {}
+    req:setopt_writefunction(table.insert, res_result)
+    req:setopt_headerfunction(table.insert, res_headers)
+
+    local ok, err = req:perform()
+    if ok then
+      -- Get request statistics if they are enabled
+      local stats_config = _G._rest_nvim.result.behavior.statistics
+      if stats_config.enable then
+        ret.statistics = get_stats(req, stats_config.stats)
+      end
+
+      -- Returns the decoded URL if the request URL was encoded by cURL to improve the results
+      -- buffer output readability
+      if should_encode_url and _G._rest_nvim.result.behavior.decode_url then
+        ret.url = request.request.url
+      else
+        ret.url = req:getinfo_effective_url()
+      end
+      ret.code = req:getinfo_response_code()
+      ret.method = req:getinfo_effective_method()
+      ret.headers = table.concat(res_headers):gsub("\r", "")
+      ret.body = table.concat(res_result)
+      -- We are returning the request script variable as it
+      ret.script = request.script
+    else
+      ---@diagnostic disable-next-line need-check-nil
+      logger:error("Something went wrong when making the request with cURL:\n" .. curl_error(err:no()))
+      return {}
+    end
+    req:close()
+  end
+
+  return ret
+end
+
+return client
diff --git a/lua/rest-nvim/commands.lua b/lua/rest-nvim/commands.lua
new file mode 100644
index 00000000..39b28e82
--- /dev/null
+++ b/lua/rest-nvim/commands.lua
@@ -0,0 +1,220 @@
+---@mod rest-nvim.commands rest.nvim commands
+---
+---@brief [[
+---
+--- `:Rest {command {args?}}`
+---
+--- command                         action
+---------------------------------------------------------------------------------
+---
+--- run {scope?}                    Execute one or several HTTP requests depending
+---                                 on given `scope`. This scope can be either `last`,
+---                                 `cursor` (default) or `document`.
+---
+--- last                            Re-run the last executed request, alias to `run last`
+---                                 to retain backwards compatibility with the old keybinds
+---                                 layout.
+---
+--- logs                            Open the rest.nvim logs file in a new tab.
+---
+--- env {action?} {path?}           Manage the environment file that is currently in use while
+---                                 running requests. If you choose to `set` the environment,
+---                                 you must provide a `path` to the environment file. The
+---                                 default action is `show`, which displays the current
+---                                 environment file path.
+---
+--- result {direction?}             Cycle through the results buffer winbar panes. The cycle
+---                                 direction can be either `next` or `prev`.
+---
+---@brief ]]
+
+---@class RestCmd
+---@field impl fun(args:string[], opts: vim.api.keyset.user_command) The command implementation
+---@field complete? fun(subcmd_arg_lead: string): string[] Command completions callback, taking the lead of the subcommand's argument
+
+local commands = {}
+
+local functions = require("rest-nvim.functions")
+
+---@type { [string]: RestCmd }
+local rest_command_tbl = {
+  run = {
+    impl = function(args)
+      local request_scope = #args == 0 and "cursor" or args[1]
+      functions.exec(request_scope)
+    end,
+    ---@return string[]
+    complete = function(args)
+      local scopes = { "last", "cursor", "document" }
+      if #args < 1 then
+        return scopes
+      end
+
+      local match = vim.tbl_filter(function(scope)
+        if string.find(scope, "^" .. args) then
+          return scope
+          ---@diagnostic disable-next-line missing-return
+        end
+      end, scopes)
+
+      return match
+    end,
+  },
+  last = {
+    impl = function(_)
+      functions.exec("last")
+    end,
+  },
+  logs = {
+    impl = function(_)
+      local logs_path = table.concat({ vim.fn.stdpath("log"), "rest.nvim.log" }, "/")
+      vim.cmd("tabedit " .. logs_path)
+    end,
+  },
+  env = {
+    impl = function(args)
+      local logger = _G._rest_nvim.logger
+
+      -- If there were no arguments for env then default to the `env("show", nil)` function behavior
+      if #args < 1 then
+        functions.env(nil, nil)
+        return
+      end
+      -- If there was only one argument and it is `set` then raise an error because we are also expecting for the env file path
+      if #args == 1 and args[1] == "set" then
+        ---@diagnostic disable-next-line need-check-nil
+        logger:error("Not enough arguments were passed to the 'env' command: 2 argument were expected, 1 was passed")
+        return
+      end
+      -- We do not need too many arguments here, complain about it please!
+      if #args > 3 then
+        ---@diagnostic disable-next-line need-check-nil
+        logger:error(
+          "Too many arguments were passed to the 'env' command: 2 arguments were expected, " .. #args .. " were passed"
+        )
+        return
+      end
+
+      functions.env(args[1], args[2])
+    end,
+    ---@return string[]
+    complete = function(args)
+      local actions = { "set", "show" }
+      if #args < 1 then
+        return actions
+      end
+
+      -- If the completion arguments have a whitespace then treat them as a table instead for easiness
+      if args:find(" ") then
+        args = vim.split(args, " ")
+      end
+      -- If the completion arguments is a table and `set` is the desired action then
+      -- return a list of files in the current working directory for completion
+      if type(args) == "table" and args[1]:match("set") then
+        return functions.find_env_files()
+      end
+
+      local match = vim.tbl_filter(function(action)
+        if string.find(action, "^" .. args) then
+          return action
+          ---@diagnostic disable-next-line missing-return
+        end
+      end, actions)
+
+      return match
+    end,
+  },
+  result = {
+    impl = function(args)
+      local logger = _G._rest_nvim.logger
+
+      if #args > 1 then
+        ---@diagnostic disable-next-line need-check-nil
+        logger:error(
+          "Too many arguments were passed to the 'result' command: 1 argument was expected, " .. #args .. " were passed"
+        )
+        return
+      end
+      if not vim.tbl_contains({ "next", "prev" }, args[1]) then
+        ---@diagnostic disable-next-line need-check-nil
+        logger:error("Unknown argument was passed to the 'result' command: 'next' or 'prev' were expected")
+        return
+      end
+
+      functions.cycle_result_pane(args[1])
+    end,
+    ---@return string[]
+    complete = function(args)
+      local cycles = { "next", "prev" }
+      if #args < 1 then
+        return cycles
+      end
+
+      local match = vim.tbl_filter(function(cycle)
+        if string.find(cycle, "^" .. args) then
+          return cycle
+          ---@diagnostic disable-next-line missing-return
+        end
+      end, cycles)
+
+      return match
+    end,
+  },
+}
+
+local function rest(opts)
+  local fargs = opts.fargs
+  local cmd = fargs[1]
+  local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {}
+  local command = rest_command_tbl[cmd]
+
+  local logger = _G._rest_nvim.logger
+
+  if not command then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("Unknown command: " .. cmd)
+    return
+  end
+
+  -- NOTE: I do not know why lua lsp is complaining about a missing parameter here
+  --       when all the `command.impl` functions expect only one parameter?
+  ---@diagnostic disable-next-line missing-argument
+  command.impl(args)
+end
+
+---@package
+function commands.init(bufnr)
+  vim.api.nvim_buf_create_user_command(bufnr, "Rest", rest, {
+    nargs = "+",
+    desc = "Run your HTTP requests",
+    complete = function(arg_lead, cmdline, _)
+      local rest_commands = vim.tbl_keys(rest_command_tbl)
+      local subcmd, subcmd_arg_lead = cmdline:match("^Rest*%s(%S+)%s(.*)$")
+      if subcmd and subcmd_arg_lead and rest_command_tbl[subcmd] and rest_command_tbl[subcmd].complete then
+        return rest_command_tbl[subcmd].complete(subcmd_arg_lead)
+      end
+      if cmdline:match("^Rest*%s+%w*$") then
+        return vim.tbl_filter(function(cmd)
+          if string.find(cmd, "^" .. arg_lead) then
+            return cmd
+            ---@diagnostic disable-next-line missing-return
+          end
+        end, rest_commands)
+      end
+    end,
+  })
+end
+
+---Register a new `:Rest` subcommand
+---@see vim.api.nvim_buf_create_user_command
+---@param name string The name of the subcommand
+---@param cmd RestCmd The implementation and optional completions
+---@package
+function commands.register_subcommand(name, cmd)
+  vim.validate({ name = { name, "string" } })
+  vim.validate({ impl = { cmd.impl, "function" }, complete = { cmd.complete, "function", true } })
+
+  rest_command_tbl[name] = cmd
+end
+
+return commands
diff --git a/lua/rest-nvim/config/check.lua b/lua/rest-nvim/config/check.lua
new file mode 100644
index 00000000..4fedae1d
--- /dev/null
+++ b/lua/rest-nvim/config/check.lua
@@ -0,0 +1,102 @@
+---@mod rest-nvim.config.check rest.nvim config validation
+---
+---@brief [[
+---
+--- rest.nvim config validation (internal)
+---
+---@brief ]]
+
+local check = {}
+
+---@param tbl table The table to validate
+---@see vim.validate
+---@return boolean is_valid
+---@return string|nil error_message
+local function validate(tbl)
+  local ok, err = pcall(vim.validate, tbl)
+  return ok or false, "Invalid config" .. (err and ": " .. err or "")
+end
+
+---Validates the configuration
+---@param cfg RestConfig
+---@return boolean is_valid
+---@return string|nil error_message
+function check.validate(cfg)
+  local ok, err = validate({
+    client = { cfg.client, "string" },
+    env_file = { cfg.env_file, "string" },
+    env_pattern = { cfg.env_pattern, "string" },
+    env_edit_command = { cfg.env_edit_command, "string" },
+    encode_url = { cfg.encode_url, "boolean" },
+    skip_ssl_verification = { cfg.skip_ssl_verification, "boolean" },
+    custom_dynamic_variables = { cfg.custom_dynamic_variables, "table" },
+    keybinds = { cfg.keybinds, "table" },
+    -- RestConfigLogs
+    level = { cfg.logs.level, "string" },
+    save = { cfg.logs.save, "boolean" },
+    -- RestConfigResult
+    result = { cfg.result, "table" },
+    -- RestConfigResultSplit
+    split = { cfg.result.split, "table" },
+    horizontal = { cfg.result.split.horizontal, "boolean" },
+    in_place = { cfg.result.split.in_place, "boolean" },
+    stay_in_current_window_after_split = { cfg.result.split.stay_in_current_window_after_split, "boolean" },
+    -- RestConfigResultBehavior
+    behavior = { cfg.result.behavior, "table" },
+    decode_url = { cfg.result.behavior.decode_url, "boolean" },
+    -- RestConfigResultInfo
+    show_info = { cfg.result.behavior.show_info, "table" },
+    url = { cfg.result.behavior.show_info.url, "boolean" },
+    headers = { cfg.result.behavior.show_info.headers, "boolean" },
+    http_info = { cfg.result.behavior.show_info.http_info, "boolean" },
+    curl_command = { cfg.result.behavior.show_info.curl_command, "boolean" },
+    -- RestConfigResultStats
+    statistics = { cfg.result.behavior.statistics, "table" },
+    statistics_enable = { cfg.result.behavior.statistics.enable, "boolean" },
+    stats = { cfg.result.behavior.statistics.stats, "table" },
+    -- RestConfigResultFormatters
+    formatters = { cfg.result.behavior.formatters, "table" },
+    json = { cfg.result.behavior.formatters.json, { "string", "function" } },
+    html = { cfg.result.behavior.formatters.html, { "string", "function" } },
+    -- RestConfigHighlight
+    highlight_enable = { cfg.highlight.enable, "boolean" },
+    timeout = { cfg.highlight.timeout, "number" },
+  })
+
+  if not ok then
+    return false, err
+  end
+  return true
+end
+
+---Recursively check a table for unrecognized keys,
+---using a default table as a reference
+---@param tbl table
+---@param default_tbl table
+---@return string[]
+function check.get_unrecognized_keys(tbl, default_tbl)
+  local unrecognized_keys = {}
+  for k, _ in pairs(tbl) do
+    unrecognized_keys[k] = true
+  end
+  for k, _ in pairs(default_tbl) do
+    unrecognized_keys[k] = false
+  end
+
+  local ret = {}
+  for k, _ in pairs(unrecognized_keys) do
+    if unrecognized_keys[k] then
+      ret[k] = k
+    end
+    if type(default_tbl[k]) == "table" and tbl[k] then
+      for _, subk in pairs(check.get_unrecognized_keys(tbl[k], default_tbl[k])) do
+        local key = k .. "." .. subk
+        ret[key] = key
+      end
+    end
+  end
+
+  return vim.tbl_keys(ret)
+end
+
+return check
diff --git a/lua/rest-nvim/config/init.lua b/lua/rest-nvim/config/init.lua
index 58ad9fe6..462395e6 100644
--- a/lua/rest-nvim/config/init.lua
+++ b/lua/rest-nvim/config/init.lua
@@ -1,74 +1,183 @@
-local M = {}
+---@mod rest-nvim.config rest.nvim configuration
+---
+---@brief [[
+---
+--- rest.nvim configuration options
+---
+---@brief ]]
 
-local config = {
-  result_split_horizontal = false,
-  result_split_in_place = false,
-  stay_in_current_window_after_split = false,
-  skip_ssl_verification = false,
+local config = {}
+
+local logger = require("rest-nvim.logger")
+
+---@class RestConfigDebug
+---@field unrecognized_configs string[] Unrecognized configuration options
+
+---@class RestConfigLogs
+---@field level string The logging level name, see `:h vim.log.levels`. Default is `"info"`
+---@field save boolean Whether to save log messages into a `.log` file. Default is `true`
+
+---@class RestConfigResult
+---@field split RestConfigResultSplit Result split window behavior
+---@field behavior RestConfigResultBehavior Result buffer behavior
+
+---@class RestConfigResultSplit
+---@field horizontal boolean Open request results in a horizontal split
+---@field in_place boolean Keep the HTTP file buffer above|left when split horizontal|vertical
+---@field stay_in_current_window_after_split boolean Stay in the current window (HTTP file) or change the focus to the results window
+
+---@class RestConfigResultBehavior
+---@field show_info RestConfigResultInfo Request results information
+---@field decode_url boolean Whether to decode the request URL query parameters to improve readability
+---@field statistics RestConfigResultStats Request results statistics
+---@field formatters RestConfigResultFormatters Formatters for the request results body. If the formatter is a function it should return two values, the formatted body and a table containing two values `found` (whether the formatter has been found or not) and `name` (the formatter name)
+
+---@class RestConfigResultInfo
+---@field url boolean Display the request URL
+---@field headers boolean Display the request headers
+---@field http_info boolean Display the request HTTP information
+---@field curl_command boolean Display the cURL command that was used for the request
+
+---@class RestConfigResultStats
+---@field enable boolean Whether enable statistics or not
+---@field stats string[]|{ [1]: string, title: string }[] Statistics to be shown, takes cURL's easy getinfo constants name
+
+---@class RestConfigResultFormatters
+---@field json string|fun(body: string): string,table JSON formatter
+---@field html string|fun(body: string): string,table HTML formatter
+
+---@class RestConfigHighlight
+---@field enable boolean Whether current request highlighting is enabled or not
+---@field timeout number Duration time of the request highlighting in milliseconds
+
+---@class RestConfig
+---@field client string The HTTP client to be used when running requests, default is `"curl"`
+---@field env_file string Environment variables file to be used for the request variables in the document
+---@field env_pattern string Environment variables file pattern for telescope.nvim
+---@field env_edit_command string Neovim command to edit an environment file, default is `"tabedit"`
+---@field encode_url boolean Encode URL before making request
+---@field skip_ssl_verification boolean Skip SSL verification, useful for unknown certificates
+---@field custom_dynamic_variables { [string]: fun(): string }[] Table of custom dynamic variables
+---@field logs RestConfigLogs Logging system configuration
+---@field result RestConfigResult Request results buffer behavior
+---@field highlight RestConfigHighlight Request highlighting
+---@field keybinds { [1]: string, [2]: string, [3]: string }[] Keybindings list
+---@field debug_info? RestConfigDebug Configurations debug information, set automatically
+---@field logger? Logger Logging system, set automatically
+
+---rest.nvim default configuration
+---@type RestConfig
+local default_config = {
+  client = "curl",
+  env_file = ".env",
+  env_pattern = ".*env.*$",
+  env_edit_command = "tabedit",
   encode_url = true,
-  highlight = {
-    enabled = true,
-    timeout = 150,
+  skip_ssl_verification = false,
+  custom_dynamic_variables = {},
+  logs = {
+    level = "info",
+    save = true,
   },
   request = {
     pre_script = function() end,
   },
   result = {
-    show_curl_command = true,
-    show_url = true,
-    show_http_info = true,
-    show_headers = true,
-    show_statistics = false,
-    formatters = {
-      json = "jq",
-      html = function(body)
-        if vim.fn.executable("tidy") == 0 then
-          return body
-        end
-        -- stylua: ignore
-        return vim.fn.system({
-          "tidy", "-i", "-q",
-          "--tidy-mark",      "no",
-          "--show-body-only", "auto",
-          "--show-errors",    "0",
-          "--show-warnings",  "0",
-          "-",
-        }, body):gsub("\n$", "")
-      end,
+    split = {
+      horizontal = false,
+      in_place = false,
+      stay_in_current_window_after_split = true,
+    },
+    behavior = {
+      decode_url = true,
+      show_info = {
+        url = true,
+        headers = true,
+        http_info = true,
+        curl_command = true,
+      },
+      statistics = {
+        enable = true,
+        ---@see https://curl.se/libcurl/c/curl_easy_getinfo.html
+        stats = {
+          { "total_time", title = "Time taken:" },
+          { "size_download_t", title = "Download size:" },
+        },
+      },
+      formatters = {
+        json = "jq",
+        html = function(body)
+          if vim.fn.executable("tidy") == 0 then
+            return body, { found = false, name = "tidy" }
+          end
+          -- stylua: ignore
+          local fmt_body = vim.fn.system({
+            "tidy",
+            "-i",
+            "-q",
+            "--tidy-mark",      "no",
+            "--show-body-only", "auto",
+            "--show-errors",    "0",
+            "--show-warnings",  "0",
+            "-",
+          }, body):gsub("\n$", "")
+
+          return fmt_body, { found = true, name = "tidy" }
+        end,
+      },
     },
   },
-  jump_to_request = false,
-  env_file = ".env",
-  env_pattern = "\\.env$",
-  env_edit_command = "tabedit",
-  custom_dynamic_variables = {},
-  yank_dry_run = true,
-  search_back = true,
+  highlight = {
+    enable = true,
+    timeout = 750,
+  },
+  ---Example:
+  ---
+  ---```lua
+  ---keybinds = {
+  ---  {
+  ---    "<localleader>rr", ":Rest run", "Run request under the cursor",
+  ---  },
+  ---  {
+  ---    "<localleader>rl", ":Rest run last", "Re-run latest request",
+  ---  },
+  ---}
+  ---
+  ---```
+  ---@see vim.keymap.set
+  keybinds = {},
 }
 
---- Get a configuration value
---- @param opt string
---- @return any
-M.get = function(opt)
-  -- If an option was passed then
-  -- return the requested option.
-  -- Otherwise, return the entire
-  -- configurations.
-  if opt then
-    return config[opt]
-  end
+---Set user-defined configurations for rest.nvim
+---@param user_configs RestConfig User configurations
+---@return RestConfig rest.nvim configuration table
+function config.set(user_configs)
+  local check = require("rest-nvim.config.check")
 
-  return config
-end
+  local conf = vim.tbl_deep_extend("force", {
+    debug_info = {
+      unrecognized_configs = check.get_unrecognized_keys(user_configs, default_config),
+    },
+  }, default_config, user_configs)
+
+  local ok, err = check.validate(conf)
 
---- Set user-defined configurations
---- @param user_configs table
---- @return table
-M.set = function(user_configs)
-  vim.validate({ user_configs = { user_configs, "table" } })
+  -- We do not want to validate `logger` value so we are setting it after the validation
+  conf.logger = logger:new({
+    level_name = conf.logs.level,
+    save_logs = conf.logs.save,
+  })
+
+  if not ok then
+    ---@cast err string
+    conf.logger:error(err)
+  end
+
+  if #conf.debug_info.unrecognized_configs > 0 then
+    conf.logger:warn("Unrecognized configs found in setup: " .. vim.inspect(conf.debug_info.unrecognized_configs))
+  end
 
-  config = vim.tbl_deep_extend("force", config, user_configs)
-  return config
+  return conf
 end
 
-return M
+return config
diff --git a/lua/rest-nvim/curl/init.lua b/lua/rest-nvim/curl/init.lua
deleted file mode 100644
index 1d7f04bb..00000000
--- a/lua/rest-nvim/curl/init.lua
+++ /dev/null
@@ -1,314 +0,0 @@
-local utils = require("rest-nvim.utils")
-local curl = require("plenary.curl")
-local log = require("plenary.log").new({ plugin = "rest.nvim" })
-local config = require("rest-nvim.config")
-
-local M = {}
--- checks if 'x' can be executed by system()
-local function is_executable(x)
-  if type(x) == "string" and vim.fn.executable(x) == 1 then
-    return true
-  elseif vim.tbl_islist(x) and vim.fn.executable(x[1] or "") == 1 then
-    return true
-  end
-
-  return false
-end
-
-local function format_curl_cmd(res)
-  local cmd = "curl"
-
-  for _, value in pairs(res) do
-    if string.sub(value, 1, 1) == "-" then
-      cmd = cmd .. " " .. value
-    else
-      cmd = cmd .. " '" .. value .. "'"
-    end
-  end
-
-  -- remote -D option
-  cmd = string.gsub(cmd, "-D '%S+' ", "")
-  return cmd
-end
-
-local function send_curl_start_event(data)
-  vim.api.nvim_exec_autocmds("User", {
-    pattern = "RestStartRequest",
-    modeline = false,
-    data = data,
-  })
-end
-
-local function send_curl_stop_event(data)
-  vim.api.nvim_exec_autocmds("User", {
-    pattern = "RestStopRequest",
-    modeline = false,
-    data = data,
-  })
-end
-
-local function create_error_handler(opts)
-  return function(err)
-    send_curl_stop_event(vim.tbl_extend("keep", { err = err }, opts))
-    error(err.message)
-  end
-end
-
-local function parse_headers(headers)
-  local parsed = {}
-  for _, header in ipairs(headers) do
-    if header ~= "" then
-      local key, value = header:match("([^:]+):%s*(.*)")
-      if key then
-        parsed[key] = value or ""
-      end
-    end
-  end
-  return parsed
-end
-
--- get_or_create_buf checks if there is already a buffer with the rest run results
--- and if the buffer does not exists, then create a new one
-M.get_or_create_buf = function()
-  local tmp_name = "rest_nvim_results"
-
-  -- Check if the file is already loaded in the buffer
-  local existing_bufnr = vim.fn.bufnr(tmp_name)
-  if existing_bufnr ~= -1 then
-    -- Set modifiable
-    vim.api.nvim_set_option_value("modifiable", true, { buf = existing_bufnr })
-    -- Prevent modified flag
-    vim.api.nvim_set_option_value("buftype", "nofile", { buf = existing_bufnr })
-    -- Delete buffer content
-    vim.api.nvim_buf_set_lines(existing_bufnr, 0, -1, false, {})
-
-    -- Make sure the filetype of the buffer is httpResult so it will be highlighted
-    vim.api.nvim_set_option_value("ft", "httpResult", { buf = existing_bufnr })
-
-    return existing_bufnr
-  end
-
-  -- Create new buffer
-  local new_bufnr = vim.api.nvim_create_buf(false, true)
-  vim.api.nvim_buf_set_name(new_bufnr, tmp_name)
-  vim.api.nvim_set_option_value("ft", "httpResult", { buf = new_bufnr })
-  vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_bufnr })
-
-  return new_bufnr
-end
-
-local function create_callback(curl_cmd, opts)
-  local method = opts.method
-  local url = opts.url
-  local script_str = opts.script_str
-
-  return function(res)
-    send_curl_stop_event(vim.tbl_extend("keep", { res = res }, opts))
-
-    if res.exit ~= 0 then
-      log.error("[rest.nvim] " .. utils.curl_error(res.exit))
-      return
-    end
-    local res_bufnr = M.get_or_create_buf()
-
-    local headers = utils.filter(res.headers, function(value)
-      return value ~= ""
-    end, false)
-
-    headers = utils.map(headers, function(value)
-      local _, _, http, status = string.find(value, "^(HTTP.*)%s+(%d+)%s*$")
-
-      if http and status then
-        return http .. " " .. utils.http_status(tonumber(status))
-      end
-
-      return value
-    end)
-
-    headers = utils.split_list(headers, function(value)
-      return string.find(value, "^HTTP.*$")
-    end)
-
-    res.headers = parse_headers(res.headers)
-
-    local content_type = res.headers[utils.key(res.headers, "content-type")]
-    if content_type then
-      local isJson = content_type:match("application/.+(json)")
-
-      if isJson then
-        content_type = "json"
-      else
-        content_type = content_type:match("application/([-a-z]+)") or content_type:match("text/(%l+)")
-      end
-    end
-
-    if script_str ~= nil then
-      local context = {
-        result = res,
-        pretty_print = vim.pretty_print,
-        json_decode = vim.fn.json_decode,
-        set_env = utils.set_env,
-        set = utils.set_context,
-      }
-      local env = { context = context }
-      setmetatable(env, { __index = _G })
-      local f = load(script_str, nil, "bt", env)
-      if f ~= nil then
-        f()
-      end
-    end
-
-    if config.get("result").show_url then
-      --- Add metadata into the created buffer (status code, date, etc)
-      -- Request statement (METHOD URL)
-      utils.write_block(res_bufnr, { method:upper() .. " " .. url }, false)
-    end
-
-    -- This can be quite verbose so let user control it
-    if config.get("result").show_curl_command then
-      utils.write_block(res_bufnr, { "Command: " .. curl_cmd }, true)
-    end
-
-    if config.get("result").show_http_info then
-      -- HTTP version, status code and its meaning, e.g. HTTP/1.1 200 OK
-      utils.write_block(res_bufnr, { "HTTP/1.1 " .. utils.http_status(res.status) }, false)
-    end
-
-    if config.get("result").show_headers then
-      -- Headers, e.g. Content-Type: application/json
-      for _, header_block in ipairs(headers) do
-        utils.write_block(res_bufnr, header_block, true)
-      end
-    end
-
-    if config.get("result").show_statistics then
-      -- Statistics, e.g. Total Time: 123.4 ms
-      local statistics
-
-      res.body, statistics = utils.parse_statistics(res.body)
-
-      utils.write_block(res_bufnr, statistics, true)
-    end
-
-    --- Add the curl command results into the created buffer
-    local formatter = config.get("result").formatters[content_type]
-    -- format response body
-    if type(formatter) == "function" then
-      local ok, out = pcall(formatter, res.body)
-      -- check if formatter ran successfully
-      if ok and out then
-        res.body = out
-      else
-        vim.api.nvim_echo({
-          {
-            string.format("Error calling formatter on response body:\n%s", out),
-            "Error",
-          },
-        }, false, {})
-      end
-    elseif is_executable(formatter) then
-      local stdout = vim.fn.system(formatter, res.body):gsub("\n$", "")
-      -- check if formatter ran successfully
-      if vim.v.shell_error == 0 then
-        res.body = stdout
-      else
-        vim.api.nvim_echo({
-          {
-            string.format("Error running formatter %s on response body:\n%s", vim.inspect(formatter), stdout),
-            "Error",
-          },
-        }, false, {})
-      end
-    end
-
-    -- append response container
-    local buf_content = "#+RESPONSE\n"
-    if utils.is_binary_content_type(content_type) then
-      buf_content = buf_content .. "Binary answer"
-    else
-      buf_content = buf_content .. res.body
-    end
-    buf_content = buf_content .. "\n#+END"
-
-    local lines = utils.split(buf_content, "\n")
-
-    utils.write_block(res_bufnr, lines)
-
-    -- Only open a new split if the buffer is not loaded into the current window
-    if vim.fn.bufwinnr(res_bufnr) == -1 then
-      local cmd_split = [[vert sb]]
-      if config.get("result_split_horizontal") then
-        cmd_split = [[sb]]
-      end
-      if config.get("result_split_in_place") then
-        cmd_split = [[bel ]] .. cmd_split
-      end
-      if config.get("stay_in_current_window_after_split") then
-        vim.cmd(cmd_split .. res_bufnr .. " | wincmd p")
-      else
-        vim.cmd(cmd_split .. res_bufnr)
-      end
-      -- Set unmodifiable state
-      vim.api.nvim_set_option_value("modifiable", false, { buf = res_bufnr })
-    end
-
-    -- Send cursor in response buffer to start
-    utils.move_cursor(res_bufnr, 1)
-
-    -- add syntax highlights for response
-    local syntax_file = vim.fn.expand(string.format("$VIMRUNTIME/syntax/%s.vim", content_type))
-
-    if vim.fn.filereadable(syntax_file) == 1 then
-      vim.cmd(string.gsub(
-        [[
-        if exists("b:current_syntax")
-          unlet b:current_syntax
-        endif
-        syn include @%s syntax/%s.vim
-        syn region %sBody matchgroup=Comment start=+\v^#\+RESPONSE$+ end=+\v^#\+END$+ contains=@%s
-
-        let b:current_syntax = "httpResult"
-      ]],
-        "%%s",
-        content_type
-      ))
-    end
-  end
-end
-
--- curl_cmd runs curl with the passed options, gets or creates a new buffer
--- and then the results are printed to the recently obtained/created buffer
--- @param opts (table) curl arguments:
---           - yank_dry_run (boolean): displays the command
---           - arguments are forwarded to plenary
-M.curl_cmd = function(opts)
-  --- Execute request pre-script if any.
-  if config.get("request").pre_script then
-    config.get("request").pre_script(opts, utils.get_variables())
-  end
-
-  -- plenary's curl module is strange in the sense that with "dry_run" it returns the command
-  -- otherwise it starts the request :/
-  local dry_run_opts = vim.tbl_extend("force", opts, { dry_run = true })
-  local res = curl[opts.method](dry_run_opts)
-  local curl_cmd = format_curl_cmd(res)
-
-  send_curl_start_event(opts)
-
-  if opts.dry_run then
-    if config.get("yank_dry_run") then
-      vim.cmd("let @+=" .. string.format("%q", curl_cmd))
-    end
-
-    vim.api.nvim_echo({ { "[rest.nvim] Request preview:\n", "Comment" }, { curl_cmd } }, false, {})
-
-    send_curl_stop_event(opts)
-    return
-  else
-    opts.callback = vim.schedule_wrap(create_callback(curl_cmd, opts))
-    opts.on_error = vim.schedule_wrap(create_error_handler(opts))
-    curl[opts.method](opts)
-  end
-end
-
-return M
diff --git a/lua/rest-nvim/functions.lua b/lua/rest-nvim/functions.lua
new file mode 100644
index 00000000..fd111bb7
--- /dev/null
+++ b/lua/rest-nvim/functions.lua
@@ -0,0 +1,211 @@
+---@mod rest-nvim.functions rest.nvim functions
+---
+---@brief [[
+---
+--- rest.nvim functions
+---
+---@brief ]]
+
+local functions = {}
+
+local found_nio, nio = pcall(require, "nio")
+
+local utils = require("rest-nvim.utils")
+local parser = require("rest-nvim.parser")
+local script_vars = require("rest-nvim.parser.script_vars")
+
+local result = require("rest-nvim.result")
+local winbar = require("rest-nvim.result.winbar")
+
+---Execute 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.
+---@param scope string Defines the request execution scope. Can be: `last`, `cursor` (default) or `document`
+function functions.exec(scope)
+  vim.validate({
+    scope = { scope, "string" },
+  })
+
+  local api = require("rest-nvim.api")
+  local env_vars = require("rest-nvim.parser.env_vars")
+
+  local logger = _G._rest_nvim.logger
+  local ok, client = pcall(require, "rest-nvim.client." .. _G._rest_nvim.client)
+  if not ok then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("The client '" .. _G._rest_nvim.client .. "' could not be found. Maybe it is not installed?")
+    return {}
+  end
+
+  -- Fallback to 'cursor' if no scope was given
+  if not scope then
+    scope = "cursor"
+  end
+
+  -- Raise an error if an invalid scope has been provided
+  if not vim.tbl_contains({ "last", "cursor", "document" }, scope) then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("Invalid scope '" .. scope .. "' provided to the 'exec' function")
+    return {}
+  end
+
+  -- TODO: implement `document` scope.
+  --
+  -- NOTE: The `document` scope may require some parser adjustments
+  local req_results = {}
+
+  if scope == "cursor" then
+    local req = parser.parse(
+      ---@diagnostic disable-next-line param-type-mismatch
+      parser.look_behind_until(parser.get_node_at_cursor(), "request")
+    )
+
+    utils.highlight(0, req.start, req.end_, api.namespace)
+
+    -- Set up a _rest_nvim_req_data Lua global table that holds the parsed request
+    -- so the values can be modified from the pre-request hooks
+    _G._rest_nvim_req_data = req
+    -- Load environment variables from the env file
+    env_vars.read_file(true)
+    -- Run pre-request hooks
+    api.exec_pre_request_hooks()
+    -- Clean the _rest_nvim_req_data global after running the pre-request hooks
+    -- as the req table will remain modified
+    _G._rest_nvim_req_data = nil
+
+    if found_nio then
+      req_results = nio
+        .run(function()
+          return client.request(req)
+        end)
+        :wait()
+    else
+      req_results = client.request(req)
+    end
+
+    ---Last HTTP request made by the user
+    ---@type Request
+    _G._rest_nvim_last_request = req
+  elseif scope == "last" then
+    local req = _G._rest_nvim_last_request
+
+    if not req then
+      ---@diagnostic disable-next-line need-check-nil
+      logger:error("Rest run last: A previously made request was not found to be executed again")
+    else
+      utils.highlight(0, req.start, req.end_, api.namespace)
+
+      -- Set up a _rest_nvim_req_data Lua global table that holds the parsed request
+      -- so the values can be modified from the pre-request hooks
+      _G._rest_nvim_req_data = req
+      -- Load environment variables from the env file
+      env_vars.read_file(true)
+      -- Run pre-request hooks
+      api.exec_pre_request_hooks()
+      -- Clean the _rest_nvim_req_data global after running the pre-request hooks
+      -- as the req table will remain modified
+      _G._rest_nvim_req_data = nil
+
+      if found_nio then
+        req_results = nio
+          .run(function()
+            return client.request(req)
+          end)
+          :wait()
+      else
+        req_results = client.request(req)
+      end
+    end
+  end
+
+  -- We should not be trying to show a result or evaluate code if the request failed
+  if not vim.tbl_isempty(req_results) then
+    local result_buf = result.get_or_create_buf()
+    result.write_res(result_buf, req_results)
+
+    -- Load the script variables
+    if req_results.script ~= nil or not req_results.script == "" then
+      script_vars.load(req_results.script, req_results)
+    end
+
+    -- Set up a _rest_nvim_res_data Lua global table that holds the request results
+    -- so the values can be modified from the post-request hooks
+    _G._rest_nvim_res_data = req_results
+    -- Run post-request hooks
+    api.exec_post_request_hooks()
+    -- Clean the _rest_nvim_res_data global after running the post-request hooks
+    -- as the req_results table will remain modified
+    _G._rest_nvim_res_data = nil
+  end
+end
+
+---Find a list of environment files starting from the current directory
+---@return string[] Environment variable files path
+function functions.find_env_files()
+  -- We are currently looking for any ".*env*" file, e.g. ".env", ".env.json"
+  --
+  -- This algorithm can be improved later on to search from a parent directory if the desired environment file
+  -- is somewhere else but in the current working directory.
+  local files = vim.fs.find(function(name, _)
+    return name:match(_G._rest_nvim.env_pattern)
+  end, { limit = math.huge, type = "file", path = "./" })
+
+  return files
+end
+
+---Manage the environment file that is currently in use while running requests
+---
+---If you choose to `set` the environment, you must provide a `path` to the environment file.
+---@param action string|nil Determines the action to be taken. Can be: `set` or `show` (default)
+---@param path string|nil Path to the environment variables file
+function functions.env(action, path)
+  -- TODO: add a `select` action later to open some kind of prompt to select one of many detected "*env*" files
+  vim.validate({
+    action = { action, { "string", "nil" } },
+    path = { path, { "string", "nil" } },
+  })
+
+  local logger = _G._rest_nvim.logger
+
+  if not action then
+    action = "show"
+  end
+
+  if not vim.tbl_contains({ "set", "show" }, action) then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("Invalid action '" .. action .. "' provided to the 'env' function")
+    return
+  end
+
+  if action == "set" then
+    ---@cast path string
+    if utils.file_exists(path) then
+      _G._rest_nvim.env_file = path
+      ---@diagnostic disable-next-line need-check-nil
+      logger:info("Current env file has been changed to: " .. _G._rest_nvim.env_file)
+    else
+      ---@diagnostic disable-next-line need-check-nil
+      logger:error("Passed environment file '" .. path .. "' was not found")
+    end
+  else
+    ---@diagnostic disable-next-line need-check-nil
+    logger:info("Current env file in use: " .. _G._rest_nvim.env_file)
+  end
+end
+
+---Cycle through the results buffer winbar panes
+---@param cycle string Cycle direction, can be: `"next"` or `"prev"`
+function functions.cycle_result_pane(cycle)
+  ---@type number
+  local idx = winbar.current_pane_index
+
+  if cycle == "next" then
+    idx = idx + 1
+  elseif cycle == "prev" then
+    idx = idx - 1
+  end
+
+  _G._rest_nvim_winbar(idx)
+end
+
+return functions
diff --git a/lua/rest-nvim/health.lua b/lua/rest-nvim/health.lua
new file mode 100644
index 00000000..f424548b
--- /dev/null
+++ b/lua/rest-nvim/health.lua
@@ -0,0 +1,126 @@
+---@mod rest-nvim.health rest.nvim healthcheck
+---
+---@brief [[
+---
+---Healthcheck module for rest.nvim
+---
+---@brief ]]
+
+local health = {}
+
+local function install_health()
+  vim.health.start("Installation")
+
+  -- Luarocks installed
+  -- we check for either luarocks system-wide or rocks.nvim as rocks.nvim can manage Luarocks installation
+  if vim.fn.executable("luarocks") ~= 1 and not vim.g.rocks_nvim_loaded then
+    vim.health.error("`Luarocks` is not installed in your system")
+  else
+    vim.health.ok("Found `luarocks` installed in your system")
+  end
+
+  -- Luarocks in `package.path`
+  local found_luarocks_in_path = string.find(package.path, "rocks")
+  if not found_luarocks_in_path then
+    vim.health.error(
+      "Luarocks PATHs were not found in your Neovim's Lua `package.path`",
+      "Check rest.nvim README to know how to add your luarocks PATHs to Neovim"
+    )
+  else
+    vim.health.ok("Found Luarocks PATHs in your Neovim's Lua `package.path`")
+  end
+
+  -- Luarocks dependencies existence checking
+  for dep, dep_info in pairs(vim.g.rest_nvim_deps) do
+    if not dep_info.found then
+      local err_advice = "Install it through `luarocks --local install " .. dep .. "`"
+      if dep:find("nvim") then
+        err_advice = "Install it through your preferred plugins manager or luarocks by using `luarocks --local install "
+          .. dep
+          .. "`"
+        -- NOTE: nvim-treesitter has a weird bug in luarocks due to the parsers installation logic so let's mark it as not recommended
+        if dep == "nvim-treesitter" then
+          err_advice = err_advice .. " (not recommended yet!)"
+        end
+      end
+
+      vim.health.error("Dependency `" .. dep .. "` was not found (" .. dep_info.error .. ")", err_advice)
+    else
+      vim.health.ok("Dependency `" .. dep .. "` was found")
+    end
+  end
+
+  -- Tree-sitter and HTTP parser
+  local found_treesitter, ts_info = pcall(require, "nvim-treesitter.info")
+  if not found_treesitter then
+    vim.health.warn(
+      "Could not check for tree-sitter `http` parser existence because `nvim-treesitter` is not installed"
+    )
+  else
+    local is_http_parser_installed = vim.tbl_contains(ts_info.installed_parsers(), "http")
+    if not is_http_parser_installed then
+      vim.health.error(
+        "Tree-sitter `http` parser is not installed (rest.nvim parsing will not work.)",
+        "Install it through `:TSInstall http` or add it to your `nvim-treesitter`'s `ensure_installed` table."
+      )
+    else
+      vim.health.ok("Tree-sitter `http` parser is installed")
+    end
+  end
+end
+
+local function configuration_health()
+  vim.health.start("Configuration")
+
+  -- Configuration options
+  local unrecognized_configs = _G._rest_nvim.debug_info.unrecognized_configs
+  if not vim.tbl_isempty(unrecognized_configs) then
+    for _, config_key in ipairs(unrecognized_configs) do
+      vim.health.warn("Unrecognized configuration option `" .. config_key .. "` found")
+    end
+  else
+    vim.health.ok("No unrecognized configuration options were found")
+  end
+
+  -- Formatters
+  local formatters = _G._rest_nvim.result.behavior.formatters
+  for ft, formatter in pairs(formatters) do
+    if type(formatter) == "string" then
+      if vim.fn.executable(formatter) ~= 1 then
+        vim.health.warn(
+          "Formatter for `"
+            .. ft
+            .. "` is set to `"
+            .. formatter
+            .. "`, however, rest.nvim could not find it in your system"
+        )
+      else
+        vim.health.ok(
+          "Formatter for `" .. ft .. "` is set to `" .. formatter .. "` and rest.nvim found it in your system"
+        )
+      end
+    elseif type(formatter) == "function" then
+      local _, fmt_meta = formatter()
+      if not fmt_meta.found then
+        vim.health.warn(
+          "Formatter for `"
+            .. ft
+            .. "` is set to `"
+            .. fmt_meta.name
+            .. "`, however, rest.nvim could not find it in your system"
+        )
+      else
+        vim.health.ok(
+          "Formatter for `" .. ft .. "` is set to `" .. fmt_meta.name .. "` and rest.nvim found it in your system"
+        )
+      end
+    end
+  end
+end
+
+function health.check()
+  install_health()
+  configuration_health()
+end
+
+return health
diff --git a/lua/rest-nvim/init.lua b/lua/rest-nvim/init.lua
index 55a27fea..2b76bc2e 100644
--- a/lua/rest-nvim/init.lua
+++ b/lua/rest-nvim/init.lua
@@ -1,246 +1,28 @@
-local backend = require("rest-nvim.request")
-local config = require("rest-nvim.config")
-local curl = require("rest-nvim.curl")
-local log = require("plenary.log").new({ plugin = "rest.nvim" })
-local utils = require("rest-nvim.utils")
-local path = require("plenary.path")
+---@mod rest-nvim rest.nvim
+---
+---@brief [[
+---
+--- A fast and asynchronous Neovim HTTP client written in Lua
+---
+---@brief ]]
 
 local rest = {}
-local Opts = {}
-local defaultRequestOpts = {
-  verbose = false,
-  highlight = false,
-}
-
-local LastOpts = {}
-
-rest.setup = function(user_configs)
-  config.set(user_configs or {})
-end
-
--- run will retrieve the required request information from the current buffer
--- and then execute curl
--- @param verbose toggles if only a dry run with preview should be executed (true = preview)
-rest.run = function(verbose)
-  local ok, result = backend.get_current_request()
-  if not ok then
-    log.error("Failed to run the http request:")
-    log.error(result)
-    vim.api.nvim_err_writeln("[rest.nvim] Failed to get the current HTTP request: " .. result)
-    return
-  end
-
-  return rest.run_request(result, { verbose = verbose })
-end
-
--- run will retrieve the required request information from the current buffer
--- and then execute curl
--- @param string filename to load
--- @param opts table
---           1. keep_going boolean keep running even when last request failed
---           2. verbose boolean
-rest.run_file = function(filename, opts)
-  log.info("Running file :" .. filename)
-  opts = vim.tbl_deep_extend(
-    "force", -- use value from rightmost map
-    defaultRequestOpts,
-    { highlight = config.get("highlight").enabled },
-    opts or {}
-  )
-
-  -- 0 on error or buffer handle
-  local new_buf = vim.api.nvim_create_buf(true, false)
-
-  vim.api.nvim_win_set_buf(0, new_buf)
-  vim.cmd.edit(filename)
-
-  local requests = backend.buf_list_requests(new_buf)
-  for _, req in pairs(requests) do
-    rest.run_request(req, opts)
-  end
-
-  return true
-end
-
--- replace variables in header values
-local function splice_headers(headers)
-  for name, value in pairs(headers) do
-    headers[name] = utils.replace_vars(value)
-  end
-  return headers
-end
-
--- return the spliced/resolved filename
--- @param string the filename w/o variables
-local function load_external_payload(fileimport_string)
-  local fileimport_spliced = utils.replace_vars(fileimport_string)
-  if path:new(fileimport_spliced):is_absolute() then
-    return fileimport_spliced
-  else
-    local file_dirname = vim.fn.expand("%:p:h")
-    local file_name = path:new(path:new(file_dirname), fileimport_spliced)
-    return file_name:absolute()
-  end
-end
-
--- @param headers table  HTTP headers
--- @param payload table of the form { external = bool, filename_tpl= path, body_tpl = string }
---                 with body_tpl an array of lines
-local function splice_body(headers, payload)
-  local external_payload = payload.external
-  local lines -- array of strings
-  if external_payload then
-    local importfile = load_external_payload(payload.filename_tpl)
-    if not utils.file_exists(importfile) then
-      error("import file " .. importfile .. " not found")
-    end
-    -- TODO we dont necessarily want to load the file, it can be slow
-    -- https://github.com/rest-nvim/rest.nvim/issues/203
-    lines = utils.read_file(importfile)
-  else
-    lines = payload.body_tpl
-  end
-  local content_type = headers[utils.key(headers, "content-type")] or ""
-  local has_json = content_type:find("application/[^ ]*json")
-
-  local body = ""
-  local vars = utils.read_variables()
-  -- nvim_buf_get_lines is zero based and end-exclusive
-  -- but start_line and stop_line are one-based and inclusive
-  -- magically, this fits :-) start_line is the CRLF between header and body
-  -- which should not be included in the body, stop_line is the last line of the body
-  for _, line in ipairs(lines) do
-    body = body .. utils.replace_vars(line, vars)
-  end
 
-  local is_json, json_body = pcall(vim.json.decode, body)
-
-  if is_json and json_body then
-    if has_json then
-      -- convert entire json body to string.
-      return vim.fn.json_encode(json_body)
-    else
-      -- convert nested tables to string.
-      for key, val in pairs(json_body) do
-        if type(val) == "table" then
-          json_body[key] = vim.fn.json_encode(val)
-        end
-      end
-      return vim.fn.json_encode(json_body)
-    end
-  end
-  return body
-end
-
--- run will retrieve the required request information from the current buffer
--- and then execute curl
--- @param req table see validate_request to check the expected format
--- @param opts table
---           1. keep_going boolean keep running even when last request failed
-rest.run_request = function(req, opts)
-  -- TODO rename result to request
-  local result = req
-  local curl_raw_args = config.get("skip_ssl_verification") and vim.list_extend(result.raw, { "-k" }) or result.raw
-  opts = vim.tbl_deep_extend(
-    "force", -- use value from rightmost map
-    defaultRequestOpts,
-    { highlight = config.get("highlight").enabled },
-    opts or {}
-  )
-
-  -- if we want to pass as a file, we pass nothing to plenary
-  local spliced_body = nil
-  if not req.body.inline and req.body.filename_tpl then
-    curl_raw_args = vim.tbl_extend("force", curl_raw_args, {
-      "--data-binary",
-      "@" .. load_external_payload(req.body.filename_tpl),
-    })
-  else
-    spliced_body = splice_body(result.headers, result.body)
-  end
-
-  if config.get("result").show_statistics then
-    local statistics_line = {}
-
-    for _, tbl in ipairs(config.get("result").show_statistics) do
-      if type(tbl) == "string" then
-        tbl = { tbl }
-      end
-
-      table.insert(statistics_line, tbl[1] .. "=%{" .. tbl[1] .. "}")
-    end
-
-    curl_raw_args = vim.tbl_extend("force", curl_raw_args, {
-      "--write-out",
-      "\\n" .. table.concat(statistics_line, "&"),
-    })
-  end
-
-  Opts = {
-    request_id = vim.loop.now(), -- request id used to correlate RestStartRequest and RestStopRequest events
-    method = result.method:lower(),
-    url = result.url,
-    -- plenary.curl can't set http protocol version
-    -- http_version = result.http_version,
-    headers = splice_headers(result.headers),
-    raw = curl_raw_args,
-    body = spliced_body,
-    dry_run = opts.verbose,
-    bufnr = result.bufnr,
-    start_line = result.start_line,
-    end_line = result.end_line,
-    script_str = result.script_str,
-  }
-
-  if not opts.verbose then
-    LastOpts = Opts
-  end
-
-  if opts.highlight then
-    backend.highlight(result.bufnr, result.start_line, result.end_line)
-  end
-
-  local success_req, req_err = pcall(curl.curl_cmd, Opts)
-
-  if not success_req then
-    vim.api.nvim_err_writeln(
-      "[rest.nvim] Failed to perform the request.\nMake sure that you have entered the proper URL and the server is running.\n\nTraceback: "
-        .. req_err
-    )
-    return false, req_err
-  end
-end
-
--- last will run the last curl request, if available
-rest.last = function()
-  if LastOpts.url == nil then
-    vim.api.nvim_err_writeln("[rest.nvim]: Last request not found")
-    return
-  end
-
-  if config.get("highlight").enabled then
-    backend.highlight(LastOpts.bufnr, LastOpts.start_line, LastOpts.end_line)
-  end
-
-  local success_req, req_err = pcall(curl.curl_cmd, LastOpts)
+local config = require("rest-nvim.config")
+local keybinds = require("rest-nvim.keybinds")
+local autocmds = require("rest-nvim.autocmds")
 
-  if not success_req then
-    vim.api.nvim_err_writeln(
-      "[rest.nvim] Failed to perform the request.\nMake sure that you have entered the proper URL and the server is running.\n\nTraceback: "
-        .. req_err
-    )
-  end
-end
+---Set up rest.nvim
+---@param user_configs RestConfig User configurations
+function rest.setup(user_configs)
+  -- Set up rest.nvim configurations
+  _G._rest_nvim = config.set(user_configs or {})
 
-rest.request = backend
+  -- Set up rest.nvim keybinds
+  keybinds.apply()
 
-rest.select_env = function(env_file)
-  if path ~= nil then
-    vim.validate({ env_file = { env_file, "string" } })
-    config.set({ env_file = env_file })
-  else
-    print("No path given")
-  end
+  -- Set up rest.nvim autocommands and commands
+  autocmds.setup()
 end
 
 return rest
diff --git a/lua/rest-nvim/keybinds.lua b/lua/rest-nvim/keybinds.lua
new file mode 100644
index 00000000..85dc9b6b
--- /dev/null
+++ b/lua/rest-nvim/keybinds.lua
@@ -0,0 +1,64 @@
+---@mod rest-nvim.autocmds rest.nvim autocommands
+---
+---@brief [[
+---
+--- rest.nvim autocommands
+---
+---@brief ]]
+
+local keybinds = {}
+
+local function legacy_keybinds()
+  -- NOTE: RestNvimPreview no longer exists
+  vim.keymap.set("n", "<Plug>RestNvim", function()
+    vim.deprecate("`<Plug>RestNvim` mapping", "`:Rest run`", "2.1.0", "rest.nvim", false)
+    vim.cmd("Rest run")
+  end)
+  vim.keymap.set("n", "<Plug>RestNvimLast", function()
+    vim.deprecate("`<Plug>RestNvimLast` mapping", "`:Rest run last`", "2.1.0", "rest.nvim", false)
+    vim.cmd("Rest run")
+  end)
+end
+
+---Apply user-defined keybinds in the rest.nvim configuration
+function keybinds.apply()
+  -- Temporarily apply legacy <Plug> keybinds
+  legacy_keybinds()
+
+  -- User-defined keybinds
+  local keybindings = _G._rest_nvim.keybinds
+  for _, keybind in ipairs(keybindings) do
+    local lhs = keybind[1]
+    local cmd = keybind[2]
+    local desc = keybind[3]
+
+    vim.validate({
+      lhs = { lhs, "string" },
+      cmd = { cmd, "string" },
+      desc = { desc, "string" },
+    })
+
+    vim.keymap.set("n", lhs, cmd, { desc = desc })
+  end
+end
+
+---Register a new keybinding
+---@see vim.keymap.set
+---
+---@param mode string Keybind mode
+---@param lhs string Keybind trigger
+---@param cmd string Command to be run
+---@param opts table Keybind options
+---@package
+function keybinds.register_keybind(mode, lhs, cmd, opts)
+  vim.validate({
+    mode = { mode, "string" },
+    lhs = { lhs, "string" },
+    cmd = { cmd, "string" },
+    opts = { opts, "table" },
+  })
+
+  vim.keymap.set(mode, lhs, cmd, opts)
+end
+
+return keybinds
diff --git a/lua/rest-nvim/logger.lua b/lua/rest-nvim/logger.lua
new file mode 100644
index 00000000..e6f49d40
--- /dev/null
+++ b/lua/rest-nvim/logger.lua
@@ -0,0 +1,168 @@
+---@mod rest-nvim.logger rest.nvim logger
+---
+---@brief [[
+---
+---Logging library for rest.nvim, slightly inspired by rmagatti/logger.nvim
+---Intended for use by internal and third-party modules.
+---
+---Default logger instance is made during the `setup` and can be accessed
+---by anyone through the `_G._rest_nvim.logger` configuration field
+---that is set automatically.
+---
+---------------------------------------------------------------------------------
+---
+---Usage:
+---
+---```lua
+---local logger = require("rest-nvim.logger"):new({ level = "debug" })
+---
+---logger:set_log_level("info")
+---
+---logger:info("This is an info log")
+--- -- [rest.nvim] INFO: This is an info log
+---```
+---
+---@brief ]]
+
+---@class Logger
+local logger = {}
+
+-- NOTE: vim.loop has been renamed to vim.uv in Neovim >= 0.10 and will be removed later
+local uv = vim.uv or vim.loop
+
+---@see vim.log.levels
+---@class LoggerLevels
+local levels = {
+  trace = vim.log.levels.TRACE,
+  debug = vim.log.levels.DEBUG,
+  info = vim.log.levels.INFO,
+  warn = vim.log.levels.WARN,
+  error = vim.log.levels.ERROR,
+}
+
+---@class LoggerConfig
+---@field level_name string Logging level name. Default is `"info"`
+---@field save_logs boolean Whether to save log messages into a `.log` file. Default is `true`
+local default_config = {
+  level_name = "info",
+  save_logs = true,
+}
+
+---Store the logger output in a file at `vim.fn.stdpath("log")`
+---@see vim.fn.stdpath
+---@param msg string Logger message to be saved
+local function store_log(msg)
+  local date = os.date("%F %r") -- 2024-01-26 01:25:05 PM
+  local log_msg = date .. " | " .. msg .. "\n"
+  local log_path = table.concat({ vim.fn.stdpath("log"), "rest.nvim.log" }, "/")
+
+  -- 644 sets read and write permissions for the owner, and it sets read-only
+  -- mode for the group and others
+  uv.fs_open(log_path, "a+", tonumber(644, 8), function(err, file)
+    if file and not err then
+      local file_pipe = uv.new_pipe(false)
+      ---@cast file_pipe uv_pipe_t
+      uv.pipe_open(file_pipe, file)
+      uv.write(file_pipe, log_msg)
+      uv.fs_close(file)
+    end
+  end)
+end
+
+---Create a new logger instance
+---@param opts LoggerConfig Logger configuration
+---@return Logger
+function logger:new(opts)
+  opts = opts or {}
+  local conf = vim.tbl_deep_extend("force", default_config, opts)
+  self.level = levels[conf.level_name]
+  self.save_logs = conf.save_logs
+
+  self.__index = function(_, index)
+    if type(self[index]) == "function" then
+      return function(...)
+        -- Make any logger function call with "." access result in the syntactic sugar ":" access
+        self[index](self, ...)
+      end
+    else
+      return self[index]
+    end
+  end
+  setmetatable(opts, self)
+
+  return self
+end
+
+---Set the log level for the logger
+---@param level string New logging level
+---@see vim.log.levels
+function logger:set_log_level(level)
+  self.level = levels[level]
+end
+
+---Log a trace message
+---@param msg string Log message
+function logger:trace(msg)
+  msg = "[rest.nvim] TRACE: " .. msg
+  if self.level == vim.log.levels.TRACE then
+    vim.notify(msg, levels.trace)
+  end
+
+  if self.save_logs then
+    store_log(msg)
+  end
+end
+
+---Log a debug message
+---@param msg string Log message
+function logger:debug(msg)
+  msg = "[rest.nvim] DEBUG: " .. msg
+  if self.level == vim.log.levels.DEBUG then
+    vim.notify(msg, levels.debug)
+  end
+
+  if self.save_logs then
+    store_log(msg)
+  end
+end
+
+---Log an info message
+---@param msg string Log message
+function logger:info(msg)
+  msg = "[rest.nvim] INFO: " .. msg
+  local valid_levels = { vim.log.levels.INFO, vim.log.levels.DEBUG }
+  if vim.tbl_contains(valid_levels, self.level) then
+    vim.notify(msg, levels.info)
+  end
+
+  if self.save_logs then
+    store_log(msg)
+  end
+end
+
+---Log a warning message
+---@param msg string Log message
+function logger:warn(msg)
+  msg = "[rest.nvim] WARN: " .. msg
+  local valid_levels = { vim.log.levels.INFO, vim.log.levels.DEBUG, vim.log.levels.WARN }
+  if vim.tbl_contains(valid_levels, self.level) then
+    vim.notify(msg, levels.warn)
+  end
+
+  if self.save_logs then
+    store_log(msg)
+  end
+end
+
+---Log an error message
+---@param msg string Log message
+function logger:error(msg)
+  msg = "[rest.nvim] ERROR: " .. msg
+  vim.notify(msg, levels.error)
+
+  if self.save_logs then
+    store_log(msg)
+  end
+end
+
+return logger
diff --git a/lua/rest-nvim/parser/dynamic_vars.lua b/lua/rest-nvim/parser/dynamic_vars.lua
new file mode 100644
index 00000000..4bf2cce1
--- /dev/null
+++ b/lua/rest-nvim/parser/dynamic_vars.lua
@@ -0,0 +1,60 @@
+---@mod rest-nvim.parser.dynamic_vars rest.nvim parsing module dynamic variables
+---
+---@brief [[
+---
+--- rest.nvim dynamic variables
+---
+---@brief ]]
+
+local dynamic_vars = {}
+
+local random = math.random
+math.randomseed(os.time())
+
+---Generate a random uuid
+---@return string
+local function uuid()
+  local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
+  ---@diagnostic disable-next-line redundant-return-value
+  return string.gsub(template, "[xy]", function(c)
+    local v = (c == "x") and random(0, 0xf) or random(8, 0xb)
+    return string.format("%x", v)
+  end)
+end
+
+---Retrieve all dynamic variables from both rest.nvim and the ones declared by
+---the user on his configuration
+---@return { [string]: fun():string }[] An array-like table of tables which contains dynamic variables definition
+function dynamic_vars.retrieve_all()
+  local user_variables = _G._rest_nvim.custom_dynamic_variables or {}
+  local rest_variables = {
+    ["$uuid"] = uuid,
+    ["$date"] = function()
+      return os.date("%Y-%m-%d")
+    end,
+    ["$timestamp"] = os.time,
+    ["$randomInt"] = function()
+      return math.random(0, 1000)
+    end,
+  }
+
+  return vim.tbl_deep_extend("force", rest_variables, user_variables)
+end
+
+---Look for a dynamic variable and evaluate it
+---@param name string The dynamic variable name
+---@return string|nil The dynamic variable value or `nil` if the dynamic variable was not found
+function dynamic_vars.read(name)
+  local logger = _G._rest_nvim.logger
+
+  local vars = dynamic_vars.retrieve_all()
+  if not vim.tbl_contains(vim.tbl_keys(vars), name) then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("The dynamic variable '" .. name .. "' was not found. Maybe it's written wrong or doesn't exist?")
+    return nil
+  end
+
+  return vars[name]()
+end
+
+return dynamic_vars
diff --git a/lua/rest-nvim/parser/env_vars.lua b/lua/rest-nvim/parser/env_vars.lua
new file mode 100644
index 00000000..5a0d2cac
--- /dev/null
+++ b/lua/rest-nvim/parser/env_vars.lua
@@ -0,0 +1,74 @@
+---@mod rest-nvim.parser.env_vars rest.nvim parsing module environment variables
+---
+---@brief [[
+---
+--- rest.nvim environment variables
+---
+---@brief ]]
+
+local env_vars = {}
+
+local utils = require("rest-nvim.utils")
+
+---Get the environment variables file filetype
+---@param env_file string The environment file path
+---@return string|nil
+local function get_env_filetype(env_file)
+  local ext = vim.fn.fnamemodify(env_file, ":e")
+  return ext == "" and nil or ext
+end
+
+---Set an environment variable for the current Neovim session
+---@param name string Variable name
+---@param value string|number|boolean Variable value
+---@see vim.env
+function env_vars.set_var(name, value)
+  vim.env[name] = value
+end
+
+---Read the environment variables file from the rest.nvim configuration
+---and store all the environment variables in the `vim.env` metatable
+---@param quiet boolean Whether to fail silently if an environment file is not found, defaults to `false`
+---@see vim.env
+function env_vars.read_file(quiet)
+  quiet = quiet or false
+  local path = _G._rest_nvim.env_file
+  local logger = _G._rest_nvim.logger
+
+  if utils.file_exists(path) then
+    local env_ext = get_env_filetype(path)
+    local file_contents = utils.read_file(path)
+
+    local variables = {}
+    if env_ext == "json" then
+      variables = vim.json.decode(file_contents)
+    else
+      local vars_tbl = vim.split(file_contents, "\n")
+      table.remove(vars_tbl, #vars_tbl)
+      for _, var in ipairs(vars_tbl) do
+        local variable = vim.split(var, "=")
+        local variable_name = variable[1]
+        local variable_value
+        -- In case some weirdo adds a `=` character to his ENV value
+        if #variable > 2 then
+          table.remove(variable, 1)
+          variable_value = table.concat(variable, "=")
+        else
+          variable_value = variable[2]
+        end
+        variables[variable_name] = variable_value
+      end
+    end
+
+    for k, v in pairs(variables) do
+      vim.env[k] = v
+    end
+  else
+    if not quiet then
+      ---@diagnostic disable-next-line need-check-nil
+      logger:error("Current environment file '" .. path .. "' was not found in the current working directory")
+    end
+  end
+end
+
+return env_vars
diff --git a/lua/rest-nvim/parser/init.lua b/lua/rest-nvim/parser/init.lua
new file mode 100644
index 00000000..e4555cbc
--- /dev/null
+++ b/lua/rest-nvim/parser/init.lua
@@ -0,0 +1,448 @@
+---@mod rest-nvim.parser rest.nvim tree-sitter parsing module
+---
+---@brief [[
+---
+---Parsing module with tree-sitter, we use tree-sitter there to extract
+---all the document nodes and their content from the HTTP files, then we
+---start doing some other internal parsing like variables expansion and so on
+---
+---@brief ]]
+
+local parser = {}
+
+local dynamic_vars = require("rest-nvim.parser.dynamic_vars")
+
+---@alias NodesList { [string]: TSNode }[]
+---@alias Variables { [string]: { type_: string, value: string|number|boolean } }[]
+
+---Check if a given `node` has a syntax error and throw an error log message if that is the case
+---@param node TSNode Tree-sitter node
+---@return boolean
+local function check_syntax_error(node)
+  if node and node:has_error() then
+    local logger = _G._rest_nvim.logger
+
+    ---Create a node range string á la `:InspectTree` view
+    ---@param n TSNode
+    ---@return string
+    local function create_node_range_str(n)
+      local s_row, s_col = n:start()
+      local e_row, e_col = n:end_()
+      local range = "["
+
+      if s_row == e_row then
+        range = range .. s_row .. ":" .. s_col .. " - " .. e_col
+      else
+        range = range .. s_row .. ":" .. s_col .. " - " .. e_row .. ":" .. e_col
+      end
+      range = range .. "]"
+      return range
+    end
+
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error(
+      "The tree-sitter node at the range " .. create_node_range_str(node) .. " has a syntax error and cannot be parsed"
+    )
+    return true
+  end
+
+  return false
+end
+
+---Get a tree-sitter node at the cursor position
+---@return TSNode|nil Tree-sitter node
+---@return string|nil Node type
+function parser.get_node_at_cursor()
+  local node = assert(vim.treesitter.get_node())
+  if check_syntax_error(node) then
+    return nil, nil
+  end
+
+  return node, node:type()
+end
+
+---Small wrapper around `vim.treesitter.get_node_text` because I do not want to
+---write it every time
+---@see vim.treesitter.get_node_text
+---@param node TSNode Tree-sitter node
+---@param source integer|string Buffer or string from which the `node` is extracted
+---@return string|nil
+local function get_node_text(node, source)
+  source = source or 0
+  if check_syntax_error(node) then
+    return nil
+  end
+
+  return vim.treesitter.get_node_text(node, source)
+end
+
+---Recursively look behind `node` until `query` node type is found
+---@param node TSNode|nil Tree-sitter node, defaults to the node at the cursor position if not passed
+---@param query string The tree-sitter node type that we are looking for
+---@return TSNode|nil
+function parser.look_behind_until(node, query)
+  local logger = _G._rest_nvim.logger
+  node = node or parser.get_node_at_cursor()
+
+  -- There are no more nodes behind the `document` one
+  ---@diagnostic disable-next-line need-check-nil
+  if node:type() == "document" then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:debug("Current node is document, which does not have any parent nodes, returning it instead")
+    return node
+  end
+
+  ---@cast node TSNode
+  if check_syntax_error(node) then
+    return nil
+  end
+
+  ---@diagnostic disable-next-line need-check-nil
+  local parent = assert(node:parent())
+  if parent:type() ~= query then
+    return parser.look_behind_until(parent, query)
+  end
+
+  return parent
+end
+
+---Traverse a request tree-sitter node and retrieve all its children nodes
+---@param req_node TSNode Tree-sitter request node
+---@return NodesList
+local function traverse_request(req_node)
+  local child_nodes = {}
+  for child, _ in req_node:iter_children() do
+    local child_type = child:type()
+    if child_type ~= "header" then
+      child_nodes[child_type] = child
+    end
+  end
+  return child_nodes
+end
+
+---Traverse a request tree-sitter node and retrieve all its children header nodes
+---@param req_node TSNode Tree-sitter request node
+---@return NodesList An array-like table containing the request header nodes
+local function traverse_headers(req_node)
+  local headers = {}
+  for child, _ in req_node:iter_children() do
+    local child_type = child:type()
+    if child_type == "header" then
+      table.insert(headers, child)
+    end
+  end
+
+  return headers
+end
+
+---Traverse the document tree-sitter node and retrieve all the `variable_declaration` nodes
+---@param document_node TSNode Tree-sitter document node
+---@return Variables
+local function traverse_variables(document_node)
+  local variables = {}
+  for child, _ in document_node:iter_children() do
+    local child_type = child:type()
+    if child_type == "variable_declaration" then
+      local var_name = assert(get_node_text(child:field("name")[1], 0))
+      local var_value = child:field("value")[1]
+      local var_type = var_value:type()
+      variables[var_name] = {
+        type_ = var_type,
+        value = assert(get_node_text(var_value, 0)),
+      }
+    end
+  end
+  return variables
+end
+
+---Parse all the variable nodes in the given node and expand them to their values
+---@param node TSNode Tree-sitter node
+---@param tree string The text where variables should be looked for
+---@param text string The text where variables should be expanded
+---@param variables Variables HTTP document variables list
+---@return string|nil The given `text` with expanded variables
+local function parse_variables(node, tree, text, variables)
+  local logger = _G._rest_nvim.logger
+  local variable_query = vim.treesitter.query.parse("http", "(variable name: (_) @name)")
+  ---@diagnostic disable-next-line missing-parameter
+  for _, nod, _ in variable_query:iter_captures(node:root(), tree) do
+    local variable_name = assert(get_node_text(nod, tree))
+    local variable_value
+
+    -- If the variable name contains a `$` symbol then try to parse it as a dynamic variable
+    if variable_name:find("^%$") then
+      variable_value = dynamic_vars.read(variable_name)
+      if variable_value then
+        return variable_value
+      end
+    end
+
+    local variable = variables[variable_name]
+    -- If the variable was not found in the document then fallback to the shell environment
+    if not variable then
+      ---@diagnostic disable-next-line need-check-nil
+      logger:debug(
+        "The variable '" .. variable_name .. "' was not found in the document, falling back to the environment ..."
+      )
+      local env_var = vim.env[variable_name]
+      if not env_var then
+        ---@diagnostic disable-next-line need-check-nil
+        logger:warn(
+          "The variable '"
+            .. variable_name
+            .. "' was not found in the document or in the environment. Returning the string as received ..."
+        )
+        return text
+      end
+      variable_value = env_var
+    else
+      variable_value = variable.value
+      if variable.type_ == "string" then
+        ---@cast variable_value string
+        variable_value = variable_value:gsub('"', "")
+      end
+    end
+    text = text:gsub("{{[%s]?" .. variable_name .. "[%s]?}}", variable_value)
+  end
+  return text
+end
+
+---Parse a request tree-sitter node
+---@param children_nodes NodesList Tree-sitter nodes
+---@param variables Variables HTTP document variables list
+---@return table A table containing the request target `url` and `method` to be used
+function parser.parse_request(children_nodes, variables)
+  local request = {}
+  for node_type, node in pairs(children_nodes) do
+    if node_type == "method" then
+      request.method = assert(get_node_text(node, 0))
+    elseif node_type == "target_url" then
+      request.url = assert(get_node_text(node, 0))
+    elseif node_type == "http_version" then
+      local http_version = assert(get_node_text(node, 0))
+      request.http_version = http_version:gsub("HTTP/", "")
+    end
+  end
+
+  -- Parse the request nodes again as a single string converted into a new AST Tree to expand the variables
+  local request_text = request.method .. " " .. request.url .. "\n"
+  local request_tree = vim.treesitter.get_string_parser(request_text, "http"):parse()[1]
+  request.url = parse_variables(request_tree:root(), request_text, request.url, variables)
+
+  return request
+end
+
+---Parse request headers tree-sitter nodes
+---@param header_nodes NodesList Tree-sitter nodes
+---@param variables Variables HTTP document variables list
+---@return table A table containing the headers in a key-value style
+function parser.parse_headers(header_nodes, variables)
+  local headers = {}
+  for _, node in ipairs(header_nodes) do
+    local name = assert(get_node_text(node:field("name")[1], 0))
+    local value = vim.trim(assert(get_node_text(node:field("value")[1], 0)))
+
+    -- This dummy request is just for the parser to be able to recognize the header node
+    -- so we can iterate over it to parse the variables
+    local dummy_request = "GET http://localhost:3333\n"
+    local header_text = name .. ": " .. value
+    local header_tree = vim.treesitter.get_string_parser(dummy_request .. header_text, "http"):parse()[1]
+
+    headers[name] = parse_variables(header_tree:root(), dummy_request .. header_text, value, variables)
+  end
+
+  return headers
+end
+
+---Recursively traverse a body table and expand all the variables
+---@param tbl table Request body
+---@return table
+local function traverse_body(tbl, variables)
+  ---Expand a variable in the given string
+  ---@param str string String where the variables are going to be expanded
+  ---@param vars Variables HTTP document variables list
+  ---@return string|number|boolean
+  local function expand_variable(str, vars)
+    local logger = _G._rest_nvim.logger
+
+    local variable_name = str:gsub("{{[%s]?", ""):gsub("[%s]?}}", ""):match(".*")
+    local variable_value
+
+    -- If the variable name contains a `$` symbol then try to parse it as a dynamic variable
+    if variable_name:find("^%$") then
+      variable_value = dynamic_vars.read(variable_name)
+      if variable_value then
+        return variable_value
+      end
+    end
+
+    local variable = vars[variable_name]
+    -- If the variable was not found in the document then fallback to the shell environment
+    if not variable then
+      ---@diagnostic disable-next-line need-check-nil
+      logger:debug(
+        "The variable '" .. variable_name .. "' was not found in the document, falling back to the environment ..."
+      )
+      local env_var = vim.env[variable_name]
+      if not env_var then
+        ---@diagnostic disable-next-line need-check-nil
+        logger:warn(
+          "The variable '"
+            .. variable_name
+            .. "' was not found in the document or in the environment. Returning the string as received ..."
+        )
+        return str
+      end
+      variable_value = env_var
+    else
+      variable_value = variable.value
+      if variable.type_ == "string" then
+        ---@cast variable_value string
+        variable_value = variable_value:gsub('"', "")
+      end
+    end
+    ---@cast variable_value string|number|boolean
+    return variable_value
+  end
+
+  for k, v in pairs(tbl) do
+    if type(v) == "table" then
+      traverse_body(v, variables)
+    end
+
+    if type(k) == "string" and k:find("{{[%s]?.*[%s]?}}") then
+      local variable_value = expand_variable(k, variables)
+      local key_value = tbl[k]
+      tbl[k] = nil
+      tbl[variable_value] = key_value
+    end
+    if type(v) == "string" and v:find("{{[%s]?.*[%s]?}}") then
+      local variable_value = expand_variable(v, variables)
+      tbl[k] = variable_value
+    end
+  end
+
+  return tbl
+end
+
+---Parse a request tree-sitter node body
+---@param children_nodes NodesList Tree-sitter nodes
+---@param variables Variables HTTP document variables list
+---@return table Decoded body table
+function parser.parse_body(children_nodes, variables)
+  local body = {}
+
+  -- TODO: handle GraphQL bodies by using a graphql parser library from luarocks
+  for node_type, node in pairs(children_nodes) do
+    if node_type == "json_body" then
+      local json_body_text = assert(get_node_text(node, 0))
+      local json_body = vim.json.decode(json_body_text, {
+        luanil = { object = true, array = true },
+      })
+      body = traverse_body(json_body, variables)
+      -- This is some metadata to be used later on
+      body.__TYPE = "json"
+    elseif node_type == "xml_body" then
+      local found_xml2lua, xml2lua = pcall(require, "xml2lua")
+      if found_xml2lua then
+        local xml_handler = require("xmlhandler.tree")
+
+        local body_handler = xml_handler:new()
+        local xml_parser = xml2lua.parser(body_handler)
+        local xml_body_text = assert(get_node_text(node, 0))
+        xml_parser:parse(xml_body_text)
+        body = traverse_body(body_handler.root, variables)
+      end
+      -- This is some metadata to be used later on
+      body.__TYPE = "xml"
+    elseif node_type == "external_body" then
+      -- < @ (identifier) (file_path name: (path))
+      -- 0 1      2                 3
+      if node:child_count() > 2 then
+        body.name = assert(get_node_text(node:child(2), 0))
+      end
+      body.path = assert(get_node_text(node:field("file_path")[1], 0))
+      -- This is some metadata to be used later on
+      body.__TYPE = "external_file"
+    elseif node_type == "form_data" then
+      local names = node:field("name")
+      local values = node:field("value")
+      if vim.tbl_count(names) > 1 then
+        for idx, name in ipairs(names) do
+          ---@type string|number|boolean
+          local value = assert(get_node_text(values[idx], 0)):gsub('"', "")
+          body[assert(get_node_text(name, 0))] = value
+        end
+      else
+        ---@type string|number|boolean
+        local value = assert(get_node_text(values[1], 0)):gsub('"', "")
+        body[assert(get_node_text(names[1], 0))] = value
+      end
+      -- This is some metadata to be used later on
+      body.__TYPE = "form"
+    end
+  end
+
+  return body
+end
+
+---Get a script variable node and return its content
+---@param req_node TSNode Tree-sitter request node
+---@return string Script variables content
+function parser.parse_script(req_node)
+  -- Get the next named sibling of the current request node,
+  -- if the request does not have any sibling or if it is not
+  -- a script_variable node then early return an empty string
+  local next_sibling = req_node:next_named_sibling()
+  ---@diagnostic disable-next-line need-check-nil
+  if not next_sibling or next_sibling and next_sibling:type() ~= "script_variable" then
+    return ""
+  end
+
+  return assert(get_node_text(next_sibling, 0))
+end
+
+---@class RequestReq
+---@field method string The request method
+---@field url string The request URL
+---@field http_version? string The request HTTP protocol
+
+---@class Request
+---@field request RequestReq
+---@field headers { [string]: string|number|boolean }[]
+---@field body table
+---@field script? string
+---@field start number
+---@field end_ number
+
+---Parse a request and return the request on itself, its headers and body
+---@param req_node TSNode Tree-sitter request node
+---@return Request Table containing the request data
+function parser.parse(req_node)
+  local ast = {
+    request = {},
+    headers = {},
+    body = {},
+    script = "",
+  }
+  local document_node = parser.look_behind_until(nil, "document")
+
+  local request_children_nodes = traverse_request(req_node)
+  local request_header_nodes = traverse_headers(req_node)
+
+  ---@cast document_node TSNode
+  local document_variables = traverse_variables(document_node)
+
+  ast.request = parser.parse_request(request_children_nodes, document_variables)
+  ast.headers = parser.parse_headers(request_header_nodes, document_variables)
+  ast.body = parser.parse_body(request_children_nodes, document_variables)
+  ast.script = parser.parse_script(req_node)
+
+  -- Request node range
+  ast.start = req_node:start()
+  ast.end_ = req_node:end_()
+
+  return ast
+end
+
+return parser
diff --git a/lua/rest-nvim/parser/script_vars.lua b/lua/rest-nvim/parser/script_vars.lua
new file mode 100644
index 00000000..777c4b94
--- /dev/null
+++ b/lua/rest-nvim/parser/script_vars.lua
@@ -0,0 +1,32 @@
+---@mod rest-nvim.parser.script_vars rest.nvim parsing module script variables
+---
+---@brief [[
+---
+--- rest.nvim script variables
+---
+---@brief ]]
+
+local script_vars = {}
+
+local env_vars = require("rest-nvim.parser.env_vars")
+
+---Load a script_variable content and evaluate it
+---@param script_str string The script variable content
+---@param res table Request response body
+function script_vars.load(script_str, res)
+  local context = {
+    result = res,
+    print = vim.print,
+    json_decode = vim.json.decode,
+    set_env = env_vars.set_var,
+  }
+  local env = { context = context }
+  setmetatable(env, { __index = _G })
+
+  local f = load(script_str, "script_variable", "bt", env)
+  if f then
+    f()
+  end
+end
+
+return script_vars
diff --git a/lua/rest-nvim/request/init.lua b/lua/rest-nvim/request/init.lua
deleted file mode 100644
index 377ae805..00000000
--- a/lua/rest-nvim/request/init.lua
+++ /dev/null
@@ -1,413 +0,0 @@
-local utils = require("rest-nvim.utils")
-local log = require("plenary.log").new({ plugin = "rest.nvim" })
-local config = require("rest-nvim.config")
-
--- get_importfile returns in case of an imported file the absolute filename
--- @param bufnr Buffer number, a.k.a id
--- @param stop_line Line to stop searching
--- @return tuple filename and whether we should inline it when invoking curl
-local function get_importfile_name(bufnr, start_line, stop_line)
-  -- store old cursor position
-  local oldpos = vim.fn.getcurpos()
-  utils.move_cursor(bufnr, start_line)
-
-  local import_line = vim.fn.search("^<", "cn", stop_line)
-  -- restore old cursor position
-  utils.move_cursor(bufnr, oldpos[2])
-
-  if import_line > 0 then
-    local fileimport_string
-    local fileimport_line
-    local fileimport_inlined
-    fileimport_line = vim.api.nvim_buf_get_lines(bufnr, import_line - 1, import_line, false)
-    -- check second char against '@' (meaning "dont inline")
-    fileimport_inlined = string.sub(fileimport_line[1], 2, 2) ~= "@"
-    fileimport_string = string.gsub(fileimport_line[1], "<@?", "", 1):gsub("^%s+", ""):gsub("%s+$", "")
-    return fileimport_inlined, fileimport_string
-  end
-  return nil
-end
-
--- get_body retrieves the body lines in the buffer and then returns
--- either a table if the body is a JSON or a raw string if it is a filename
--- Plenary.curl allows a table or a raw string as body and can distinguish
--- between strings with filenames and strings with the raw body
--- @param bufnr Buffer number, a.k.a id
--- @param start_line Line where body starts
--- @param stop_line Line where body stops
--- @return table { external = bool; filename_tpl or body_tpl; }
-local function get_body(bufnr, start_line, stop_line)
-  -- first check if the body should be imported from an external file
-  local inline, importfile = get_importfile_name(bufnr, start_line, stop_line)
-  local lines -- an array of strings
-  if importfile ~= nil then
-    return { external = true, inline = inline, filename_tpl = importfile }
-  else
-    lines = vim.api.nvim_buf_get_lines(bufnr, start_line, stop_line, false)
-  end
-
-  -- nvim_buf_get_lines is zero based and end-exclusive
-  -- but start_line and stop_line are one-based and inclusive
-  -- magically, this fits :-) start_line is the CRLF between header and body
-  -- which should not be included in the body, stop_line is the last line of the body
-  local lines2 = {}
-  for _, line in ipairs(lines) do
-    -- stop if a script opening tag is found
-    if line:find("{%%") then
-      break
-    end
-    -- Ignore commented lines with and without indent
-    if not utils.contains_comments(line) then
-      lines2[#lines2 + 1] = line
-    end
-  end
-
-  return { external = false, inline = false, body_tpl = lines2 }
-end
-
-local function get_response_script(bufnr, start_line, stop_line)
-  local all_lines = vim.api.nvim_buf_get_lines(bufnr, start_line, stop_line, false)
-  -- Check if there is a script
-  local script_start_rel
-  for i, line in ipairs(all_lines) do
-    -- stop if a script opening tag is found
-    if line:find("{%%") then
-      script_start_rel = i
-      break
-    end
-  end
-
-  if script_start_rel == nil then
-    return nil
-  end
-
-  -- Convert the relative script line number to the line number of the buffer
-  local script_start = start_line + script_start_rel - 1
-
-  local script_lines = vim.api.nvim_buf_get_lines(bufnr, script_start, stop_line, false)
-  local script_str = ""
-
-  for _, line in ipairs(script_lines) do
-    script_str = script_str .. line .. "\n"
-    if line:find("%%}") then
-      break
-    end
-  end
-
-  return script_str:match("{%%(.-)%%}")
-end
-
--- is_request_line checks if the given line is a http request line according to RFC 2616
-local function is_request_line(line)
-  local http_methods = { "GET", "POST", "PUT", "PATCH", "DELETE" }
-  for _, method in ipairs(http_methods) do
-    if line:find("^" .. method) then
-      return true
-    end
-  end
-  return false
-end
-
--- get_headers retrieves all the found headers and returns a lua table with them
--- @param bufnr Buffer number, a.k.a id
--- @param start_line Line where the request starts
--- @param end_line Line where the request ends
-local function get_headers(bufnr, start_line, end_line)
-  local headers = {}
-  local headers_end = end_line
-
-  -- Iterate over all buffer lines starting after the request line
-  for line_number = start_line + 1, end_line do
-    local line_content = vim.fn.getbufline(bufnr, line_number)[1]
-
-    -- message header and message body are separated by CRLF (see RFC 2616)
-    -- for our purpose also the next request line will stop the header search
-    if is_request_line(line_content) or line_content == "" then
-      headers_end = line_number
-      break
-    end
-    if not line_content:find(":") then
-      log.warn("Missing Key/Value pair in message header. Ignoring line: ", line_content)
-      goto continue
-    end
-
-    local header_name, header_value = line_content:match("^(.-): ?(.*)$")
-
-    if not utils.contains_comments(header_name) then
-      headers[header_name] = header_value
-    end
-    ::continue::
-  end
-
-  return headers, headers_end
-end
-
--- get_curl_args finds command line flags and returns a lua table with them
--- @param bufnr Buffer number, a.k.a id
--- @param headers_end Line where the headers end
--- @param end_line Line where the request ends
-local function get_curl_args(bufnr, headers_end, end_line)
-  local curl_args = {}
-  local body_start = end_line
-
-  log.debug("Getting curl args between lines", headers_end, " and ", end_line)
-  for line_number = headers_end, end_line do
-    local line_content = vim.fn.getbufline(bufnr, line_number)[1]
-
-    if line_content:find("^ *%-%-?[a-zA-Z%-]+") then
-      local lc = vim.split(line_content, " ")
-      local x = ""
-
-      for i, y in ipairs(lc) do
-        x = x .. y
-
-        if #y:match("\\*$") % 2 == 1 and i ~= #lc then
-          -- insert space if there is an slash at end
-          x = x .. " "
-        else
-          -- insert 'x' into curl_args and reset it
-          table.insert(curl_args, x)
-          x = ""
-        end
-      end
-    elseif not line_content:find("^ *$") then
-      if line_number ~= end_line then
-        body_start = line_number - 1
-      end
-      break
-    end
-  end
-
-  return curl_args, body_start
-end
-
--- start_request will find the request line (e.g. POST http://localhost:8081/foo)
--- of the current request and returns the linenumber of this request line.
--- The current request is defined as the next request line above the cursor
--- @param bufnr The buffer number of the .http-file
--- @param linenumber (number) From which line to start looking
-local function start_request(bufnr, linenumber)
-  log.debug("Searching pattern starting from " .. linenumber)
-
-  local oldlinenumber = linenumber
-  utils.move_cursor(bufnr, linenumber)
-
-  local res
-  if config.get("search_back") then
-    res = vim.fn.search("^GET\\|^POST\\|^PUT\\|^PATCH\\|^DELETE", "cb")
-  else
-    res = vim.fn.search("^GET\\|^POST\\|^PUT\\|^PATCH\\|^DELETE", "cn")
-  end
-  -- restore cursor position
-  utils.move_cursor(bufnr, oldlinenumber)
-
-  return res
-end
-
--- end_request will find the next request line (e.g. POST http://localhost:8081/foo)
--- and returns the linenumber before this request line or the end of the buffer
--- @param bufnr The buffer number of the .http-file
-local function end_request(bufnr, linenumber)
-  -- store old cursor position
-  local oldlinenumber = linenumber
-  local last_line = vim.fn.line("$")
-
-  -- start searching for next request from the next line
-  -- as the current line does contain the current, not the next request
-  if linenumber < last_line then
-    linenumber = linenumber + 1
-  end
-  utils.move_cursor(bufnr, linenumber)
-
-  local next = vim.fn.search("^GET\\|^POST\\|^PUT\\|^PATCH\\|^DELETE\\|^###\\", "cnW")
-
-  -- restore cursor position
-  utils.move_cursor(bufnr, oldlinenumber)
-
-  if next == 0 or (oldlinenumber == last_line) then
-    return last_line
-  else
-    -- skip comment lines above requests
-    while vim.fn.getline(next - 1):find("^ *#") do
-      next = next - 1
-    end
-
-    return next - 1
-  end
-end
-
--- parse_url returns a table with the method of the request and the URL
--- @param stmt the request statement, e.g., POST http://localhost:3000/foo
-local function parse_url(stmt)
-  -- remove HTTP
-  local parsed = utils.split(stmt, " HTTP/")
-  local http_version = nil
-  if parsed[2] ~= nil then
-    http_version = parsed[2]
-  end
-  parsed = utils.split(parsed[1], " ")
-  local http_method = parsed[1]
-  table.remove(parsed, 1)
-  local target_url = table.concat(parsed, " ")
-
-  target_url = utils.replace_vars(target_url)
-  if config.get("encode_url") then
-    -- Encode URL
-    target_url = utils.encode_url(target_url)
-  end
-
-  return {
-    method = http_method,
-    http_version = http_version,
-    url = target_url,
-  }
-end
-
-local M = {}
-M.get_current_request = function()
-  return M.buf_get_request(vim.api.nvim_win_get_buf(0), vim.fn.getcurpos())
-end
-
--- buf_get_request returns a table with all the request settings
--- @param bufnr (number|nil) the buffer number
--- @param curpos the cursor position
--- @return (boolean, request or string)
-M.buf_get_request = function(bufnr, curpos)
-  curpos = curpos or vim.fn.getcurpos()
-  bufnr = bufnr or vim.api.nvim_win_get_buf(0)
-
-  local start_line = start_request(bufnr, curpos[2])
-
-  if start_line == 0 then
-    return false, "No request found"
-  end
-  local end_line = end_request(bufnr, start_line)
-
-  local parsed_url = parse_url(vim.fn.getline(start_line))
-
-  local headers, headers_end = get_headers(bufnr, start_line, end_line)
-
-  local curl_args, body_start = get_curl_args(bufnr, headers_end, end_line)
-
-  local host = headers[utils.key(headers, "host")] or ""
-
-  if string.sub(parsed_url.url, 1, 1) == "/" then
-    parsed_url.url = host:gsub("%s+", "") .. parsed_url.url
-    headers[utils.key(headers, "host")] = nil
-  end
-
-  local body = get_body(bufnr, body_start, end_line)
-
-  local script_str = get_response_script(bufnr, headers_end, end_line)
-
-  -- TODO this should just parse the request without modifying external state
-  -- eg move to run_request
-  if config.get("jump_to_request") then
-    utils.move_cursor(bufnr, start_line)
-  else
-    utils.move_cursor(bufnr, curpos[2], curpos[3])
-  end
-
-  local req = {
-    method = parsed_url.method,
-    url = parsed_url.url,
-    http_version = parsed_url.http_version,
-    headers = headers,
-    raw = curl_args,
-    body = body,
-    bufnr = bufnr,
-    start_line = start_line,
-    end_line = end_line,
-    script_str = script_str,
-  }
-
-  return true, req
-end
-
-M.print_request = function(req)
-  print(M.stringify_request(req))
-end
-
--- converts request into string, helpful for debug
--- full_body boolean
-M.stringify_request = function(req, opts)
-  opts = vim.tbl_deep_extend(
-    "force", -- use value from rightmost map
-    { full_body = false, headers = true }, -- defaults
-    opts or {}
-  )
-  local str = [[
-    url   : ]] .. req.url .. [[\n
-    method: ]] .. req.method .. [[\n
-    range : ]] .. tostring(req.start_line) .. [[ -> ]] .. tostring(req.end_line) .. [[\n
-    ]]
-
-  if req.http_version then
-    str = str .. "\nhttp_version: " .. req.http_version .. "\n"
-  end
-
-  if opts.headers then
-    for name, value in pairs(req.headers) do
-      str = str .. "header '" .. name .. "'=" .. value .. "\n"
-    end
-  end
-
-  if opts.full_body then
-    if req.body then
-      local res = req.body
-      str = str .. "body: " .. res .. "\n"
-    end
-  end
-
-  -- here we should just display the beginning of the request
-  return str
-end
-
-M.buf_list_requests = function(buf, _opts)
-  local last_line = vim.fn.line("$")
-  local requests = {}
-
-  -- reset cursor position
-  vim.fn.cursor({ 1, 1 })
-  local curpos = vim.fn.getcurpos()
-  log.debug("Listing requests for buf ", buf)
-  while curpos[2] <= last_line do
-    local ok, req = M.buf_get_request(buf, curpos)
-    if ok then
-      curpos[2] = req.end_line + 1
-      requests[#requests + 1] = req
-    else
-      break
-    end
-  end
-  -- log.debug("found " , #requests , "requests")
-  return requests
-end
-
-local select_ns = vim.api.nvim_create_namespace("rest-nvim")
-M.highlight = function(bufnr, start_line, end_line)
-  local opts = config.get("highlight") or {}
-  local higroup = "IncSearch"
-  local timeout = opts.timeout or 150
-
-  vim.api.nvim_buf_clear_namespace(bufnr, select_ns, 0, -1)
-
-  local end_column = string.len(vim.fn.getline(end_line))
-
-  vim.highlight.range(
-    bufnr,
-    select_ns,
-    higroup,
-    { start_line - 1, 0 },
-    { end_line - 1, end_column },
-    { regtype = "c", inclusive = false }
-  )
-
-  vim.defer_fn(function()
-    if vim.api.nvim_buf_is_valid(bufnr) then
-      vim.api.nvim_buf_clear_namespace(bufnr, select_ns, 0, -1)
-    end
-  end, timeout)
-end
-
-return M
diff --git a/lua/rest-nvim/result/help.lua b/lua/rest-nvim/result/help.lua
new file mode 100644
index 00000000..557dd39b
--- /dev/null
+++ b/lua/rest-nvim/result/help.lua
@@ -0,0 +1,110 @@
+---@mod rest-nvim.result.help rest.nvim result buffer help
+---
+---@brief [[
+---
+--- rest.nvim result buffer help window handling
+---
+---@brief ]]
+
+local help = {}
+
+local result = require("rest-nvim.result")
+
+---Get or create a new request window help buffer
+local function get_or_create_buf()
+  local tmp_name = "rest_winbar_help"
+  local existing_buf, help_bufnr = false, nil
+
+  -- Check if the help buffer is already loaded
+  for _, id in ipairs(vim.api.nvim_list_bufs()) do
+    if vim.api.nvim_buf_get_name(id):find(tmp_name) then
+      existing_buf = true
+      help_bufnr = id
+    end
+  end
+
+  if not existing_buf then
+    -- Create a new buffer
+    local new_bufnr = vim.api.nvim_create_buf(false, true)
+    vim.api.nvim_buf_set_name(new_bufnr, tmp_name)
+    vim.api.nvim_set_option_value("ft", "markdown", { buf = new_bufnr })
+    vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_bufnr })
+
+    -- Write to buffer
+    local buf_content = {
+      "**`rest.nvim` results window help**",
+      "",
+      "**Keybinds**:",
+      "  - `H`: go to previous pane",
+      "  - `L`: go to next pane",
+      "  - `q`: close results window",
+      "",
+      "**Press `q` to close this help window**",
+    }
+    result.write_block(new_bufnr, buf_content, false, false)
+
+    return new_bufnr
+  end
+
+  return help_bufnr
+end
+
+---Open the request results help window
+function help.open()
+  local help_bufnr = get_or_create_buf()
+
+  -- Get the results buffer window ID
+  local winnr
+  for _, id in ipairs(vim.api.nvim_list_wins()) do
+    if vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(id)):find("rest_nvim_results") then
+      winnr = id
+    end
+  end
+
+  -- Help window sizing and positioning
+  local width = vim.api.nvim_win_get_width(winnr) / 2
+  local height = 8
+
+  local col = vim.api.nvim_win_get_width(winnr) - width - 4
+  local row = vim.api.nvim_win_get_height(winnr) - height - 4
+
+  -- Display the help buffer window
+  ---@cast help_bufnr number
+  local help_win = vim.api.nvim_open_win(help_bufnr, true, {
+    style = "minimal",
+    border = "single",
+    win = winnr,
+    relative = "win",
+    width = width,
+    height = height,
+    row = row,
+    col = col,
+  })
+
+  -- Always conceal the markdown content
+  vim.api.nvim_set_option_value("conceallevel", 2, { win = help_win })
+  vim.api.nvim_set_option_value("concealcursor", "n", { win = help_win })
+end
+
+---Close the request results help window
+function help.close()
+  local logger = _G._rest_nvim.logger
+
+  -- Get the help buffer ID
+  local winnr
+  for _, id in ipairs(vim.api.nvim_list_wins()) do
+    if vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(id)):find("rest_winbar_help") then
+      winnr = id
+    end
+  end
+
+  if not winnr then
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("Could not find a help window to close")
+    return
+  end
+
+  vim.api.nvim_win_close(winnr, false)
+end
+
+return help
diff --git a/lua/rest-nvim/result/init.lua b/lua/rest-nvim/result/init.lua
new file mode 100644
index 00000000..9b401121
--- /dev/null
+++ b/lua/rest-nvim/result/init.lua
@@ -0,0 +1,326 @@
+---@mod rest-nvim.result rest.nvim result buffer
+---
+---@brief [[
+---
+--- rest.nvim result buffer handling
+---
+---@brief ]]
+
+local result = {}
+
+local found_nio, nio = pcall(require, "nio")
+
+local winbar = require("rest-nvim.result.winbar")
+
+---Results buffer handler number
+---@type number|nil
+result.bufnr = nil
+
+---Select the winbar panel based on the pane index and set the pane contents
+---
+---If the pane index is higher than 3 or lower than 1, it will cycle through
+---the panes, e.g. >= 4 gets converted to 1 and <= 0 gets converted to 3
+---@param selected number winbar pane index
+_G._rest_nvim_winbar = function(selected)
+  winbar.set_pane(selected)
+  -- Set winbar pane contents
+  ---@diagnostic disable-next-line undefined-field
+  result.write_block(result.bufnr, winbar.pane_map[winbar.current_pane_index].contents, true, false)
+end
+
+---Move the cursor to the desired position in the given buffer
+---@param bufnr number Buffer handler number
+---@param row number The desired line
+---@param col number The desired column, defaults to `1`
+local function move_cursor(bufnr, row, col)
+  col = col or 1
+  vim.api.nvim_buf_call(bufnr, function()
+    vim.fn.cursor(row, col)
+  end)
+end
+
+---Check if there is already a buffer with the rest run results
+---and create the buffer if it does not exist
+---@see vim.api.nvim_create_buf
+---@return number Buffer handler number
+function result.get_or_create_buf()
+  local tmp_name = "rest_nvim_results"
+
+  -- Check if the file is already loaded in the buffer
+  local existing_buf, bufnr = false, nil
+  for _, id in ipairs(vim.api.nvim_list_bufs()) do
+    if vim.api.nvim_buf_get_name(id):find(tmp_name) then
+      existing_buf = true
+      bufnr = id
+    end
+  end
+
+  if existing_buf then
+    -- Set modifiable
+    vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr })
+    -- Prevent modified flag
+    vim.api.nvim_set_option_value("buftype", "nofile", { buf = bufnr })
+    -- Delete buffer content
+    ---@cast bufnr number
+    vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {})
+
+    -- Make sure the filetype of the buffer is `httpResult` so it will be highlighted
+    vim.api.nvim_set_option_value("ft", "httpResult", { buf = bufnr })
+
+    result.bufnr = bufnr
+    return bufnr
+  end
+
+  -- Create a new buffer
+  local new_bufnr = vim.api.nvim_create_buf(true, true)
+  vim.api.nvim_buf_set_name(new_bufnr, tmp_name)
+  vim.api.nvim_set_option_value("ft", "httpResult", { buf = new_bufnr })
+  vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_bufnr })
+
+  result.bufnr = new_bufnr
+  return new_bufnr
+end
+
+---Wrapper around `vim.api.nvim_buf_set_lines`
+---@param bufnr number The target buffer
+---@param block string[] The list of lines to write
+---@param rewrite boolean? Rewrite the buffer content, defaults to `true`
+---@param newline boolean? Add a newline to the end, defaults to `false`
+---@see vim.api.nvim_buf_set_lines
+function result.write_block(bufnr, block, rewrite, newline)
+  rewrite = rewrite or true
+  newline = newline or false
+
+  local content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+  local first_line = false
+
+  if (#content == 1 and content[1] == "") or rewrite then
+    first_line = true
+  end
+
+  if rewrite then
+    -- Set modifiable state
+    vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr })
+    -- Delete buffer content
+    vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {})
+  end
+
+  vim.api.nvim_buf_set_lines(bufnr, first_line and 0 or -1, -1, false, block)
+
+  if newline then
+    vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
+  end
+
+  -- Set unmodifiable state
+  vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr })
+end
+
+---Display results buffer window
+---@param bufnr number The target buffer
+---@param stats table Request statistics
+function result.display_buf(bufnr, stats)
+  local is_result_displayed = false
+
+  -- Check if the results buffer is already displayed
+  for _, id in ipairs(vim.api.nvim_list_wins()) do
+    if vim.api.nvim_win_get_buf(id) == bufnr then
+      is_result_displayed = true
+      break
+    end
+  end
+
+  if not is_result_displayed then
+    local cmd = "vert sb"
+
+    local split_behavior = _G._rest_nvim.result.split
+    if split_behavior.horizontal then
+      cmd = "sb"
+    elseif split_behavior.in_place then
+      cmd = "bel " .. cmd
+    end
+
+    if split_behavior.stay_in_current_window_after_split then
+      vim.cmd(cmd .. bufnr .. " | wincmd p")
+    else
+      vim.cmd(cmd .. bufnr)
+    end
+
+    -- Get the ID of the window that contains the results buffer
+    local winnr
+    for _, id in ipairs(vim.api.nvim_list_wins()) do
+      if vim.api.nvim_win_get_buf(id) == bufnr then
+        winnr = id
+      end
+    end
+
+    -- Disable concealing for the results buffer window
+    vim.api.nvim_set_option_value("conceallevel", 0, { win = winnr })
+
+    -- Disable numbering for the results buffer window
+    vim.api.nvim_set_option_value("number", false, { win = winnr })
+    vim.api.nvim_set_option_value("relativenumber", false, { win = winnr })
+
+    -- Enable wrapping and smart indent on break
+    vim.api.nvim_set_option_value("wrap", true, { win = winnr })
+    vim.api.nvim_set_option_value("breakindent", true, { win = winnr })
+
+    -- Set winbar pane contents
+    ---@diagnostic disable-next-line undefined-field
+    result.write_block(bufnr, winbar.pane_map[winbar.current_pane_index].contents, true, false)
+
+    -- Set unmodifiable state
+    vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr })
+
+    -- Set winbar
+    winbar.set_hl() -- Set initial highlighting before displaying winbar
+    vim.wo[winnr].winbar = winbar.get_content(stats)
+  end
+
+  -- Set winbar pane contents
+  ---@diagnostic disable-next-line undefined-field
+  result.write_block(bufnr, winbar.pane_map[winbar.current_pane_index].contents, true, false)
+  move_cursor(bufnr, 1, 1)
+end
+
+---Format the result body
+---@param bufnr number The target buffer
+---@param headers table Request headers
+---@param res table Request results
+local function format_body(bufnr, headers, res)
+  local logger = _G._rest_nvim.logger
+
+  ---@type string
+  local res_type
+  for _, header in ipairs(headers) do
+    if header:find("^Content%-Type") then
+      local content_type = vim.trim(vim.split(header, ":")[2])
+      -- We need to remove the leading charset if we are getting a JSON
+      res_type = vim.split(content_type, "/")[2]:gsub(";.*", "")
+    end
+  end
+
+  -- Do not try to format binary content
+  local body = {}
+  if res_type == "octet-stream" then
+    body = { "Binary answer" }
+  else
+    local formatters = _G._rest_nvim.result.behavior.formatters
+    local filetypes = vim.tbl_keys(formatters)
+
+    -- If there is a formatter for the content type filetype then
+    -- format the request result body, otherwise return it as we got it
+    if vim.tbl_contains(filetypes, res_type) then
+      local fmt = formatters[res_type]
+      if type(fmt) == "function" then
+        local ok, out = pcall(fmt, res.body)
+        if ok and out then
+          res.body = out
+        else
+          ---@diagnostic disable-next-line need-check-nil
+          logger:error("Error calling formatter on response body:\n" .. out)
+        end
+      elseif vim.fn.executable(fmt) == 1 then
+        local stdout = vim.fn.system(fmt, res.body):gsub("\n$", "")
+        -- Check if formatter ran successfully
+        if vim.v.shell_error == 0 then
+          res.body = stdout
+        else
+          ---@diagnostic disable-next-line need-check-nil
+          logger:error("Error running formatter '" .. fmt .. "' on response body:\n" .. stdout)
+        end
+      end
+    elseif res_type ~= nil then
+      ---@diagnostic disable-next-line need-check-nil
+      logger:info(
+        "Could not find a formatter for the body type "
+          .. res_type
+          .. " returned in the request, the results will not be formatted"
+      )
+    end
+
+    body = vim.split(res.body, "\n")
+    table.insert(body, 1, res.method .. " " .. res.url)
+    table.insert(body, 2, headers[1]) -- HTTP/X and status code + meaning
+    table.insert(body, 3, "")
+    table.insert(body, 4, "#+RES")
+    table.insert(body, "#+END")
+
+    -- Remove the HTTP/X and status code + meaning from here to avoid duplicates
+    ---@diagnostic disable-next-line undefined-field
+    table.remove(winbar.pane_map[2].contents, 1)
+
+    -- add syntax highlights for response
+    if res_type ~= nil then
+      vim.api.nvim_buf_call(bufnr, function()
+        local syntax_file = vim.fn.expand(string.format("$VIMRUNTIME/syntax/%s.vim", res_type))
+        if vim.fn.filereadable(syntax_file) == 1 then
+          vim.cmd(string.gsub(
+            [[
+            if exists("b:current_syntax")
+              unlet b:current_syntax
+            endif
+            syn include @%s syntax/%s.vim
+            syn region %sBody matchgroup=Comment start=+\v^#\+RES$+ end=+\v^#\+END$+ contains=@%s
+
+            let b:current_syntax = "httpResult"
+            ]],
+            "%%s",
+            res_type
+          ))
+        end
+      end)
+    end
+  end
+  ---@diagnostic disable-next-line inject-field
+  winbar.pane_map[1].contents = body
+end
+
+---Write request results in the given buffer and display it
+---@param bufnr number The target buffer
+---@param res table Request results
+function result.write_res(bufnr, res)
+  local headers = vim.tbl_filter(function(header)
+    if header ~= "" then
+      return header
+      ---@diagnostic disable-next-line missing-return
+    end
+  end, vim.split(res.headers, "\n"))
+
+  local cookies = vim.tbl_filter(function(header)
+    if header:find("set%-cookie") then
+      return header
+      ---@diagnostic disable-next-line missing-return
+    end
+  end, headers)
+
+  --
+  -- Content-Type: application/json
+  --
+  ---@diagnostic disable-next-line inject-field
+  winbar.pane_map[2].contents = headers
+
+  ---@diagnostic disable-next-line inject-field
+  winbar.pane_map[3].contents = vim.tbl_isempty(cookies) and { "No cookies" } or cookies
+
+  if found_nio then
+    nio.run(function()
+      format_body(bufnr, headers, res)
+    end)
+  else
+    format_body(bufnr, headers, res)
+  end
+
+  -- Add statistics to the response
+  local stats = {}
+  table.sort(res.statistics)
+  for _, stat in pairs(res.statistics) do
+    table.insert(stats, stat)
+  end
+  table.sort(stats)
+  ---@diagnostic disable-next-line inject-field
+  winbar.pane_map[4].contents = stats
+
+  result.display_buf(bufnr, res.statistics)
+end
+
+return result
diff --git a/lua/rest-nvim/result/winbar.lua b/lua/rest-nvim/result/winbar.lua
new file mode 100644
index 00000000..30464712
--- /dev/null
+++ b/lua/rest-nvim/result/winbar.lua
@@ -0,0 +1,111 @@
+---@mod rest-nvim.result.winbar rest.nvim result buffer winbar add-on
+---
+---@brief [[
+---
+--- rest.nvim result buffer winbar
+---
+---@brief ]]
+
+local winbar = {}
+
+---Current pane index in the results window winbar
+---@type number
+winbar.current_pane_index = 1
+
+---Create the winbar contents and return them
+---@param stats table Request statistics
+---@return string
+function winbar.get_content(stats)
+  -- winbar panes
+  local content =
+    [[%#Normal# %1@v:lua._G._rest_nvim_winbar@%#ResponseHighlight#Response%X%#Normal# %#RestText#|%#Normal# %2@v:lua._G._rest_nvim_winbar@%#HeadersHighlight#Headers%X%#Normal# %#RestText#|%#Normal# %3@v:lua._G._rest_nvim_winbar@%#CookiesHighlight#Cookies%X%#Normal# %#RestText#|%#Normal# %4@v:lua._G._rest_nvim_winbar@%#StatsHighlight#Stats%X%#Normal# %=%<]]
+
+  -- winbar statistics
+  if not vim.tbl_isempty(stats) then
+    for stat_name, stat_value in pairs(stats) do
+      local val = vim.split(stat_value, ": ")
+      if stat_name:find("total_time") then
+        content = content .. " %#RestText# " .. val[1]:lower() .. ": "
+        local value, representation = vim.split(val[2], " ")[1], vim.split(val[2], " ")[2]
+        content = content .. "%#Number#" .. value .. " %#Normal#" .. representation
+      elseif stat_name:find("size_download") then
+        content = content .. " %#RestText#" .. val[1]:lower() .. ": "
+        local value, representation = vim.split(val[2], " ")[1], vim.split(val[2], " ")[2]
+        content = content .. "%#Number#" .. value .. " %#Normal#" .. representation
+      end
+    end
+    content = content .. " %#RestText#|%#Normal# "
+  end
+  -- content = content .. "%#RestText#Press %#Keyword#H%#RestText# for the prev pane or %#Keyword#L%#RestText# for the next pane%#Normal# "
+  content = content .. "%#RestText#Press %#Keyword#?%#RestText# for help%#Normal# "
+
+  return content
+end
+
+---@class ResultPane
+---@field name string Pane name
+---@field contents string[] Pane contents
+
+---Results window winbar panes list
+---@type { [number]: ResultPane }[]
+winbar.pane_map = {
+  [1] = { name = "Response", contents = { "Fetching ..." } },
+  [2] = { name = "Headers", contents = { "Fetching ..." } },
+  [3] = { name = "Cookies", contents = { "Fetching ..." } },
+  [4] = { name = "Stats", contents = { "Fetching ..." } },
+}
+
+---Get the foreground value of a highlighting group
+---@param name string Highlighting group name
+---@return string
+local function get_hl_group_fg(name)
+  -- If the HEX color has a zero as the first character, `string.format` will skip it
+  -- so we have to add it manually later
+  local hl_fg = string.format("%02X", vim.api.nvim_get_hl(0, { name = name, link = false }).fg)
+  if #hl_fg == 5 then
+    hl_fg = "0" .. hl_fg
+  end
+  hl_fg = "#" .. hl_fg
+  return hl_fg
+end
+
+---Set the results window winbar highlighting groups
+function winbar.set_hl()
+  -- Set highlighting for the winbar panes name
+  local textinfo_fg = get_hl_group_fg("Statement")
+  for i, pane in ipairs(winbar.pane_map) do
+    ---@diagnostic disable-next-line undefined-field
+    vim.api.nvim_set_hl(0, pane.name .. "Highlight", {
+      fg = textinfo_fg,
+      bold = (i == winbar.current_pane_index),
+      underline = (i == winbar.current_pane_index),
+    })
+  end
+
+  -- Set highlighting for the winbar text
+  local textmuted_fg = get_hl_group_fg("Comment")
+  vim.api.nvim_set_hl(0, "RestText", { fg = textmuted_fg })
+end
+
+---Select the winbar panel based on the pane index and set the pane contents
+---
+---If the pane index is higher than 4 or lower than 1, it will cycle through
+---the panes, e.g. >= 5 gets converted to 1 and <= 0 gets converted to 4
+---@param selected number winbar pane index
+function winbar.set_pane(selected)
+  if type(selected) == "number" then
+    winbar.current_pane_index = selected
+  end
+
+  -- Cycle through the panes
+  if winbar.current_pane_index > 4 then
+    winbar.current_pane_index = 1
+  end
+  if winbar.current_pane_index < 1 then
+    winbar.current_pane_index = 4
+  end
+
+  winbar.set_hl()
+end
+
+return winbar
diff --git a/lua/rest-nvim/utils.lua b/lua/rest-nvim/utils.lua
new file mode 100644
index 00000000..ad3ddd36
--- /dev/null
+++ b/lua/rest-nvim/utils.lua
@@ -0,0 +1,153 @@
+---@mod rest-nvim.utils rest.nvim utilities
+---
+---@brief [[
+---
+--- rest.nvim utility functions
+---
+---@brief ]]
+
+local utils = {}
+
+-- NOTE: vim.loop has been renamed to vim.uv in Neovim >= 0.10 and will be removed later
+local uv = vim.uv or vim.loop
+
+---Encodes a string into its escaped hexadecimal representation
+---taken from Lua Socket and added underscore to ignore
+---@param str string Binary string to be encoded
+---@return string
+function utils.escape(str)
+  local encoded = string.gsub(str, "([^A-Za-z0-9_])", function(c)
+    return string.format("%%%02x", string.byte(c))
+  end)
+
+  return encoded
+end
+
+---Check if a file exists in the given `path`
+---@param path string file path
+---@return boolean
+function utils.file_exists(path)
+  ---@diagnostic disable-next-line undefined-field
+  local fd = uv.fs_open(path, "r", 438)
+  if fd then
+    ---@diagnostic disable-next-line undefined-field
+    uv.fs_close(fd)
+    return true
+  end
+
+  return false
+end
+
+---Read a file if it exists
+---@param path string file path
+---@return string
+function utils.read_file(path)
+  local logger = _G._rest_nvim.logger
+
+  ---@type string|nil
+  local content
+  if utils.file_exists(path) then
+    ---@diagnostic disable-next-line undefined-field
+    local file = uv.fs_open(path, "r", 438)
+    ---@diagnostic disable-next-line undefined-field
+    local stat = uv.fs_fstat(file)
+    ---@diagnostic disable-next-line undefined-field
+    content = uv.fs_read(file, stat.size, 0)
+    ---@diagnostic disable-next-line undefined-field
+    uv.fs_close(file)
+  else
+    ---@diagnostic disable-next-line need-check-nil
+    logger:error("Failed to read file '" .. path .. "'")
+    return ""
+  end
+
+  ---@cast content string
+  return content
+end
+
+--- Default transformers for statistics
+local transform = {
+  ---Transform `time` into a readable typed time (e.g. 200ms)
+  ---@param time string
+  ---@return string
+  time = function(time)
+    ---@diagnostic disable-next-line cast-local-type
+    time = tonumber(time)
+
+    if time >= 60 then
+      time = string.format("%.2f", time / 60)
+
+      return time .. " min"
+    end
+
+    local units = { "s", "ms", "µs", "ns" }
+    local unit = 1
+
+    while time < 1 and unit <= #units do
+      ---@diagnostic disable-next-line cast-local-type
+      time = time * 1000
+      unit = unit + 1
+    end
+
+    time = string.format("%.2f", time)
+
+    return time .. " " .. units[unit]
+  end,
+
+  ---Transform `bytes` into another bigger size type if needed
+  ---@param bytes string
+  ---@return string
+  size = function(bytes)
+    ---@diagnostic disable-next-line cast-local-type
+    bytes = tonumber(bytes)
+
+    local units = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" }
+    local unit = 1
+
+    while bytes >= 1024 and unit <= #units do
+      ---@diagnostic disable-next-line cast-local-type
+      bytes = bytes / 1024
+      unit = unit + 1
+    end
+
+    bytes = string.format("%.2f", bytes)
+
+    return bytes .. " " .. units[unit]
+  end,
+}
+
+utils.transform_time = transform.time
+utils.transform_size = transform.size
+
+---Highlight a request
+---@param bufnr number Buffer handler ID
+---@param start number Request tree-sitter node start
+---@param end_ number Request tree-sitter node end
+---@param ns number rest.nvim Neovim namespace
+function utils.highlight(bufnr, start, end_, ns)
+  local highlight = _G._rest_nvim.highlight
+  local higroup = "IncSearch"
+  local timeout = highlight.timeout
+
+  -- Clear buffer highlights
+  vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
+
+  -- Highlight request
+  vim.highlight.range(
+    bufnr,
+    ns,
+    higroup,
+    { start, 0 },
+    { end_, string.len(vim.fn.getline(end_)) },
+    { regtype = "c", inclusive = false }
+  )
+
+  -- Clear buffer highlights again after timeout
+  vim.defer_fn(function()
+    if vim.api.nvim_buf_is_valid(bufnr) then
+      vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
+    end
+  end, timeout)
+end
+
+return utils
diff --git a/lua/rest-nvim/utils/init.lua b/lua/rest-nvim/utils/init.lua
deleted file mode 100644
index 3867c20e..00000000
--- a/lua/rest-nvim/utils/init.lua
+++ /dev/null
@@ -1,801 +0,0 @@
-local config = require("rest-nvim.config")
-
-local random = math.random
-math.randomseed(os.time())
-
-local M = {}
-local contexts = {}
-
-M.binary_content_types = {
-  "octet-stream",
-}
-
-M.is_binary_content_type = function(content_type)
-  return vim.tbl_contains(M.binary_content_types, content_type)
-end
-
--- move_cursor moves the cursor to the desired position in the provided buffer
--- @param bufnr Buffer number, a.k.a id
--- @param line the desired line
--- @param column the desired column, defaults to 1
-M.move_cursor = function(bufnr, line, column)
-  column = column or 1
-  vim.api.nvim_buf_call(bufnr, function()
-    vim.fn.cursor(line, column)
-  end)
-end
-
-M.set_env = function(key, value)
-  local variables = M.get_env_variables()
-  variables[key] = value
-  M.write_env_file(variables)
-end
-
--- set_context sets a context variable for the current file
--- @param key The key to set
--- @param value The value to set
-M.set_context = function(key, value)
-  local env_file = "/" .. (config.get("env_file") or ".env")
-  local context = contexts[env_file] or {}
-  context[key] = value
-  contexts[env_file] = context
-end
-
-M.write_env_file = function(variables)
-  local env_file = "/" .. (config.get("env_file") or ".env")
-
-  -- Directories to search for env files
-  local env_file_paths = {
-    -- current working directory
-    vim.fn.getcwd() .. env_file,
-    -- directory of the currently opened file
-    vim.fn.expand("%:p:h") .. env_file,
-  }
-
-  -- If there's an env file in the current working dir
-  for _, env_file_path in ipairs(env_file_paths) do
-    if M.file_exists(env_file_path) then
-      local file = io.open(env_file_path, "w+")
-      if file ~= nil then
-        if string.match(env_file_path, "(.-)%.json$") then
-          file:write(vim.fn.json_encode(variables))
-        else
-          for key, value in pairs(variables) do
-            file:write(key .. "=" .. value .. "\n")
-          end
-        end
-        file:close()
-      end
-    end
-  end
-end
-
-M.uuid = function()
-  local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
-  return string.gsub(template, "[xy]", function(c)
-    local v = (c == "x") and random(0, 0xf) or random(8, 0xb)
-    return string.format("%x", v)
-  end)
-end
-
--- file_exists checks if the provided file exists and returns a boolean
--- @param file File to check
-M.file_exists = function(file)
-  return vim.fn.filereadable(file) == 1
-end
-
--- read_file Reads all lines from a file and returns the content as a table
--- returns empty table if file does not exist
-M.read_file = function(file)
-  if not M.file_exists(file) then
-    return {}
-  end
-  local lines = {}
-  for line in io.lines(file) do
-    lines[#lines + 1] = line
-  end
-  return lines
-end
-
--- reads the variables contained in the current file
-M.get_file_variables = function()
-  local variables = {}
-
-  -- If there is a line at the beginning with @ first
-  if vim.fn.search("^@", "cn") > 0 then
-    -- Read all lines of the file
-    local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
-
-    -- For each line
-    for _, line in pairs(lines) do
-      -- Get the name and value form lines that starts with @
-      local name, val = line:match("^@([%w!@#$%^&*-_+?~]+)%s*=%s*([^=]+)")
-      if name then
-        -- Add to variables
-        variables[name] = val
-      end
-    end
-  end
-  return variables
-end
-
--- Gets the variables from the currently selected env_file
-M.get_env_variables = function()
-  local variables = {}
-  local env_file = "/" .. (config.get("env_file") or ".env")
-
-  -- Directories to search for env files
-  local env_file_paths = {
-    -- current working directory
-    vim.fn.getcwd() .. env_file,
-    -- directory of the currently opened file
-    vim.fn.expand("%:p:h") .. env_file,
-  }
-
-  -- If there's an env file in the current working dir
-  for _, env_file_path in ipairs(env_file_paths) do
-    if M.file_exists(env_file_path) then
-      if string.match(env_file_path, "(.-)%.json$") then
-        local f = io.open(env_file_path, "r")
-        if f ~= nil then
-          local json_vars = f:read("*all")
-          variables = vim.fn.json_decode(json_vars)
-          f:close()
-        end
-      else
-        for line in io.lines(env_file_path) do
-          local vars = M.split(line, "%s*=%s*", 1)
-          variables[vars[1]] = vars[2]
-        end
-      end
-    end
-  end
-  return variables
-end
-
-M.get_context_variables = function()
-  local env_file = "/" .. (config.get("env_file") or ".env")
-  return contexts[env_file] or {}
-end
-
--- get_variables Reads the environment variables found in the env_file option
--- (default: .env) specified in configuration or from the files being read
--- with variables beginning with @ and returns a table with the variables
-M.get_variables = function()
-  local variables = {}
-  local file_variables = M.get_file_variables()
-  local env_variables = M.get_env_variables()
-
-  for k, v in pairs(file_variables) do
-    variables[k] = v
-  end
-
-  for k, v in pairs(env_variables) do
-    variables[k] = v
-  end
-
-  -- For each variable name
-  for name, _ in pairs(variables) do
-    -- For each pair of variables
-    for oname, ovalue in pairs(variables) do
-      -- If a variable contains another variable
-      if variables[name]:match(oname) then
-        -- Add that into the variable
-        -- I.E if @url={{path}}:{{port}}/{{source}}
-        -- Substitute in path, port and source
-        variables[name] = variables[name]:gsub("{{" .. oname .. "}}", ovalue)
-      end
-    end
-  end
-
-  return variables
-end
-
-M.read_dynamic_variables = function()
-  local from_config = config.get("custom_dynamic_variables") or {}
-  local dynamic_variables = {
-    ["$uuid"] = M.uuid,
-    ["$timestamp"] = os.time,
-    ["$randomInt"] = function()
-      return math.random(0, 1000)
-    end,
-  }
-  for k, v in pairs(from_config) do
-    dynamic_variables[k] = v
-  end
-  return dynamic_variables
-end
-
-M.get_node_value = function(node, bufnr)
-  local start_row, start_col, _, end_col = node:range()
-  local line = vim.api.nvim_buf_get_lines(bufnr, start_row, start_row + 1, false)[1]
-  return line and string.sub(line, start_col + 1, end_col):gsub("^[\"'](.*)[\"']$", "%1") or nil
-end
-
-M.read_document_variables = function()
-  local variables = {}
-  local bufnr = vim.api.nvim_get_current_buf()
-  local parser = vim.treesitter.get_parser(bufnr)
-  if not parser then
-    return variables
-  end
-
-  local first_tree = parser:trees()[1]
-  if not first_tree then
-    return variables
-  end
-
-  local root = first_tree:root()
-  if not root then
-    return variables
-  end
-
-  for node in root:iter_children() do
-    local type = node:type()
-    if type == "header" then
-      local name = node:named_child(0)
-      local value = node:named_child(1)
-      variables[M.get_node_value(name, bufnr)] = M.get_node_value(value, bufnr)
-    elseif type ~= "comment" then
-      break
-    end
-  end
-  return variables
-end
-
-M.read_variables = function()
-  local first = M.get_variables()
-  local second = M.get_context_variables()
-  local third = M.read_dynamic_variables()
-  local fourth = M.read_document_variables()
-
-  return vim.tbl_extend("force", first, second, third, fourth)
-end
-
--- replace_vars replaces the env variables fields in the provided string
--- with the env variable value
--- @param str Where replace the placers for the env variables
-M.replace_vars = function(str, vars)
-  if vars == nil then
-    vars = M.read_variables()
-  end
-  -- remove $dotenv tags, which are used by the vscode rest client for cross compatibility
-  str = str:gsub("%$dotenv ", ""):gsub("%$DOTENV ", "")
-
-  for var in string.gmatch(str, "{{[^}]+}}") do
-    var = var:gsub("{", ""):gsub("}", "")
-    -- If the env variable wasn't found in the `.env` file or in the dynamic variables then search it
-    -- in the OS environment variables
-    if M.has_key(vars, var) then
-      str = type(vars[var]) == "function" and str:gsub("{{" .. var .. "}}", vars[var]())
-        or str:gsub("{{" .. var .. "}}", vars[var])
-    else
-      if os.getenv(var) then
-        str = str:gsub("{{" .. var .. "}}", os.getenv(var))
-      else
-        error(string.format("Environment variable '%s' was not found.", var))
-      end
-    end
-  end
-  return str
-end
-
--- has_key checks if the provided table contains the provided key using a regex
--- @param tbl Table to iterate over
--- @param key The key to be searched in the table
-M.has_key = function(tbl, key)
-  for tbl_key, _ in pairs(tbl) do
-    if string.find(key, tbl_key) then
-      return true
-    end
-  end
-  return false
-end
-
--- has_value checks if the provided table contains the provided string using a regex
--- @param tbl Table to iterate over
--- @param str String to search in the table
-M.has_value = function(tbl, str)
-  for _, element in ipairs(tbl) do
-    if string.find(str, element) then
-      return true
-    end
-  end
-  return false
-end
-
--- key returns the provided table's key that matches the given case-insensitive pattern.
--- if not found, return the given key.
--- @param tbl Table to iterate over
--- @param key The key to be searched in the table
-M.key = function(tbl, key)
-  for tbl_key, _ in pairs(tbl) do
-    if string.lower(tbl_key) == string.lower(key) then
-      return tbl_key
-    end
-  end
-  return key
-end
-
--- tbl_to_str recursively converts the provided table into a json string
--- @param tbl Table to convert into a String
--- @param json If the string should use a key:value syntax
-M.tbl_to_str = function(tbl, json)
-  if not json then
-    json = false
-  end
-  local result = "{"
-  for k, v in pairs(tbl) do
-    -- Check the key type (ignore any numerical keys - assume its an array)
-    if type(k) == "string" then
-      result = result .. '"' .. k .. '"' .. ":"
-    end
-    -- Check the value type
-    if type(v) == "table" then
-      result = result .. M.tbl_to_str(v)
-    elseif type(v) == "boolean" then
-      result = result .. tostring(v)
-    elseif type(v) == "number" then
-      result = result .. v
-    else
-      result = result .. '"' .. v .. '"'
-    end
-    if json then
-      result = result .. ":"
-    else
-      result = result .. ","
-    end
-  end
-  -- Remove leading commas from the result
-  if result ~= "" then
-    result = result:sub(1, result:len() - 1)
-  end
-  return result .. "}"
-end
-
--- Just a split function because Lua does not have this, nothing more
--- @param str String to split
--- @param sep Separator
--- @param max_splits Number of times to split the string (optional)
-M.split = function(str, sep, max_splits)
-  if sep == nil then
-    sep = "%s"
-  end
-  max_splits = max_splits or -1
-
-  local str_tbl = {}
-  local nField, nStart = 1, 1
-  local nFirst, nLast = str:find(sep, nStart)
-  while nFirst and max_splits ~= 0 do
-    str_tbl[nField] = str:sub(nStart, nFirst - 1)
-    nField = nField + 1
-    nStart = nLast + 1
-    nFirst, nLast = str:find(sep, nStart)
-    max_splits = max_splits - 1
-  end
-  str_tbl[nField] = str:sub(nStart)
-
-  return str_tbl
-end
-
--- iter_lines returns an iterator
--- @param str String to iterate over
-M.iter_lines = function(str)
-  -- If the string does not have a newline at the end then add it manually
-  if str:sub(-1) ~= "\n" then
-    str = str .. "\n"
-  end
-
-  return str:gmatch("(.-)\n")
-end
-
--- char_to_hex returns the provided character as its hex value, e.g., "[" is
--- converted to "%5B"
--- @param char The character to convert
-M.char_to_hex = function(char)
-  return string.format("%%%02X", string.byte(char))
-end
-
--- encode_url encodes the given URL
--- @param url The URL to encode
-M.encode_url = function(url)
-  if url == nil then
-    error("You must need to provide an URL to encode")
-  end
-
-  url = url:gsub("\n", "\r\n")
-  -- Encode characters but exclude `.`, `_`, `-`, `:`, `/`, `?`, `&`, `=`, `~`, `@`
-  url = string.gsub(url, "([^%w _ %- . : / ? & = ~ @])", M.char_to_hex)
-  url = url:gsub(" ", "+")
-  return url
-end
-
--- contains_comments checks if the given string contains comments characters
--- @param str The string that should be checked
--- @return number
-M.contains_comments = function(str)
-  return str:find("^#") or str:find("^%s+#")
-end
-
---- Filter a table and return filtered copy
----
---- @param tbl table The table to filter
---- @param filter function The filtering function, parameters are value, key and table
---- @param preserve_keys boolean? Should the copied table preserve keys or not, default true
----
---- @return List|table
-M.filter = function(tbl, filter, preserve_keys)
-  local out = {}
-
-  preserve_keys = preserve_keys and true
-
-  for key, val in ipairs(tbl) do
-    if filter(val, key, tbl) then
-      if preserve_keys then
-        out[key] = val
-      else
-        table.insert(out, val)
-      end
-    end
-  end
-
-  return out
-end
-
---- Make a copy of the table applying the transformation function to each element.
---- Does not preserve the keys of the original table.
----
---- @param tbl table The table to filter
---- @param transform function The transformation function, parameters are value, key and table
----
---- @return List
-M.map = function(tbl, transform)
-  local out = {}
-
-  for key, val in ipairs(tbl) do
-    table.insert(out, transform(val, key, tbl))
-  end
-
-  return out
-end
-
---- Wrapper around nvim_buf_set_lines
----
---- @param buffer integer The target buffer
---- @param block List The list of lines to write
---- @param newline boolean? Add a newline to the end, default false
----
---- @return nil
-M.write_block = function(buffer, block, newline)
-  local content = vim.api.nvim_buf_get_lines(buffer, 0, -1, false)
-  local first_line = false
-
-  if #content == 1 and content[1] == "" then
-    first_line = true
-  end
-
-  vim.api.nvim_buf_set_lines(buffer, first_line and 0 or -1, -1, false, block)
-
-  if newline then
-    vim.api.nvim_buf_set_lines(buffer, -1, -1, false, { "" })
-  end
-end
-
---- Split table on the elements where the function returns true
----
---- @param tbl List
---- @param index function
---- @param inclusive boolean? If true the split value is in the first table, default false
----
---- @return List[]
-M.split_list = function(tbl, index, inclusive)
-  local out = { {} }
-
-  for key, val in ipairs(tbl) do
-    if index(val, key, tbl) then
-      table.insert(out, {})
-
-      if inclusive then
-        table.insert(out[#out - 1], val)
-      else
-        table.insert(out[#out], val)
-      end
-    else
-      table.insert(out[#out], val)
-    end
-  end
-
-  return out
-end
-
---- Default transformers for statistics
-local transform = {
-  time = function(time)
-    time = tonumber(time)
-
-    if time >= 60 then
-      time = string.format("%.2f", time / 60)
-
-      return time .. " min"
-    end
-
-    local units = { "s", "ms", "µs", "ns" }
-    local unit = 1
-
-    while time < 1 and unit <= #units do
-      time = time * 1000
-      unit = unit + 1
-    end
-
-    time = string.format("%.2f", time)
-
-    return time .. " " .. units[unit]
-  end,
-
-  size = function(bytes)
-    bytes = tonumber(bytes)
-
-    local units = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" }
-    local unit = 1
-
-    while bytes >= 1024 and unit <= #units do
-      bytes = bytes / 1024
-      unit = unit + 1
-    end
-
-    bytes = string.format("%.2f", bytes)
-
-    return bytes .. " " .. units[unit]
-  end,
-}
-
---- Parse statistics line to a table with key, value pairs
----
---- @param statistics_line string The statistics line from body
----
---- @return string[] statistics
-local get_parsed_statistics = function(statistics_line)
-  local out = {}
-
-  for _, statistics_pair in ipairs(M.split(statistics_line, "&")) do
-    local value = M.split(statistics_pair, "=", 1)
-
-    if #value == 1 then
-      table.insert(out, value[1])
-    else
-      out[value[1]] = value[2]
-    end
-  end
-
-  return out
-end
-
---- Parse and transform statistics line to a table of strings to be output.
---- Returns the body without statistics line and a table of statistics lines.
----
---- @param body string Response body
----
---- @return string body, string[] statistics
-M.parse_statistics = function(body)
-  local _, _, statistics = string.find(body, "[%c%s]+([^%c]*)$")
-  local config_statistics = config.get("result").show_statistics
-
-  body = string.gsub(body, "[%c%s]+([^%c]*)$", "")
-  local out = {}
-
-  statistics = get_parsed_statistics(statistics)
-
-  for _, tbl in ipairs(config_statistics) do
-    if type(tbl) == "string" then
-      tbl = { tbl }
-    end
-
-    local value = statistics[tbl[1]]
-
-    if tbl.type then
-      if type(tbl.type) == "string" then
-        value = transform[tbl.type](value)
-      end
-
-      if type(tbl.type) == "function" then
-        value = tbl.type(value)
-      end
-    else
-      for key, fun in pairs(transform) do
-        if string.match(tbl[1], "^" .. key) then
-          value = fun(value)
-        end
-      end
-    end
-
-    table.insert(out, (tbl.title or (tbl[1] .. " ")) .. value)
-  end
-
-  return body, out
-end
-
--- http_status returns the status code and the meaning, e.g. 200 OK
--- see https://httpstatuses.com/ for reference
--- @param code The request status code
-M.http_status = function(code)
-  -- NOTE: this table does not cover all the statuses _yet_
-  local status_meaning = {
-    -- 1xx codes (Informational)
-    [100] = "Continue",
-    [101] = "Switching Protocols",
-    [102] = "Processing",
-
-    -- 2xx codes (Success)
-    [200] = "OK",
-    [201] = "Created",
-    [202] = "Accepted",
-    [203] = "Non-authoritative Information",
-    [204] = "No Content",
-    [205] = "Reset Content",
-    [206] = "Partial Content",
-    [207] = "Multi-Status",
-    [208] = "Already Reported",
-    [226] = "IM Used",
-
-    -- 3xx codes (Redirection)
-    [300] = "Multiple Choices",
-    [301] = "Moved Permanently",
-    [302] = "Found",
-    [303] = "See Other",
-    [304] = "Not Modified",
-    [305] = "Use Proxy",
-    [307] = "Temporary Redirect",
-    [308] = "Permanent Redirect",
-
-    -- 4xx codes (Client Error)
-    [400] = "Bad Request",
-    [401] = "Unauthorized",
-    [403] = "Forbidden",
-    [404] = "Not Found",
-    [405] = "Method Not Allowed",
-    [406] = "Not Acceptable",
-    [407] = "Proxy Authentication Required",
-    [408] = "Request Timeout",
-    [409] = "Conflict",
-    [410] = "Gone",
-    [411] = "Length Required",
-    [412] = "Precondition Failed",
-    [413] = "Payload Too Large",
-    [414] = "Request-URI Too Long",
-    [415] = "Unsupported Media Type",
-    [416] = "Requested Range Not Satisfiable",
-    [417] = "Expectation Failed",
-    [418] = "I'm a teapot",
-    [421] = "Misdirected Request",
-    [422] = "Unprocessable Entity",
-    [423] = "Locked",
-    [424] = "Failed Dependency",
-    [426] = "Upgrade Required",
-    [428] = "Precondition Required",
-    [429] = "Too Many Requests",
-    [431] = "Request Header Fields Too Large",
-    [444] = "Connection Closed Without Response",
-    [451] = "Unavailable For Legal Reasons",
-    [499] = "Client Closed Request",
-
-    -- 5xx codes (Server Error)
-    [500] = "Internal Server Error",
-    [501] = "Not Implemented",
-    [502] = "Bad Gateway",
-    [503] = "Service Unavailable",
-    [504] = "Gateway Timeout",
-    [505] = "HTTP Version Not Supported",
-    [506] = "Variant Also Negotiates",
-    [507] = "Insufficient Storage",
-    [508] = "Loop Detected",
-    [510] = "Not Extended",
-    [511] = "Network Authentication Required",
-    [599] = "Network Connect Timeout Error",
-  }
-
-  -- If the code is covered in the status_meaning table
-  if status_meaning[code] ~= nil then
-    return tostring(code) .. " " .. status_meaning[code]
-  end
-
-  return tostring(code) .. " Unknown Status Meaning"
-end
-
--- curl_error returns the status code and the meaning of an curl error
--- see man curl for reference
--- @param code The exit code of curl
-M.curl_error = function(code)
-  local curl_error_dictionary = {
-    [1] = "Unsupported protocol. This build of curl has no support for this protocol.",
-    [2] = "Failed to initialize.",
-    [3] = "URL malformed. The syntax was not correct.",
-    [4] = "A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time."
-      .. "To make curl able to do this, you probably need another build of libcurl!",
-    [5] = "Couldn't resolve proxy. The given proxy host could not be resolved.",
-    [6] = "Couldn't resolve host. The given remote host was not resolved.",
-    [7] = "Failed to connect to host.",
-    [8] = "Weird server reply. The server sent data curl couldn't parse.",
-    [9] = "FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn't exist on the server.",
-    [10] = "FTP accept failed. While waiting for the server to connect back when an active FTP session is used, an error code was sent over the control connection or similar.",
-    [11] = "FTP weird PASS reply. Curl couldn't parse the reply sent to the PASS request.",
-    [12] = "During an active FTP session while waiting for the server to connect back to curl, the timeout expired.",
-    [13] = "FTP weird PASV reply, Curl couldn't parse the reply sent to the PASV request.",
-    [14] = "FTP weird 227 format. Curl couldn't parse the 227-line the server sent.",
-    [15] = "FTP can't get host. Couldn't resolve the host IP we got in the 227-line.",
-    [16] = "HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems, see the error message for details.",
-    [17] = "FTP couldn't set binary. Couldn't change transfer method to binary.",
-    [18] = "Partial file. Only a part of the file was transferred.",
-    [19] = "FTP couldn't download/access the given file, the RETR (or similar) command failed.",
-    [21] = "FTP quote error. A quote command returned error from the server.",
-    [22] = "HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used.",
-    [23] = "Write error. Curl couldn't write data to a local filesystem or similar.",
-    [25] = "FTP couldn't STOR file. The server denied the STOR operation, used for FTP uploading.",
-    [26] = "Read error. Various reading problems.",
-    [27] = "Out of memory. A memory allocation request failed.",
-    [28] = "Operation timeout. The specified time-out period was reached according to the conditions.",
-    [30] = "FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!",
-    [31] = "FTP couldn't use REST. The REST command failed. This command is used for resumed FTP transfers.",
-    [33] = 'HTTP range error. The range "command" didn\'t work.',
-    [34] = "HTTP post error. Internal post-request generation error.",
-    [35] = "SSL connect error. The SSL handshaking failed.",
-    [36] = "Bad download resume. Couldn't continue an earlier aborted download.",
-    [37] = "FILE couldn't read file. Failed to open the file. Permissions?",
-    [38] = "LDAP cannot bind. LDAP bind operation failed.",
-    [39] = "LDAP search failed.",
-    [41] = "Function not found. A required LDAP function was not found.",
-    [42] = "Aborted by callback. An application told curl to abort the operation.",
-    [43] = "Internal error. A function was called with a bad parameter.",
-    [45] = "Interface error. A specified outgoing interface could not be used.",
-    [47] = "Too many redirects. When following redirects, curl hit the maximum amount.",
-    [48] = "Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was passed on to libcurl and rejected. Read up in the manual!",
-    [49] = "Malformed telnet option.",
-    [51] = "The peer's SSL certificate or SSH MD5 fingerprint was not OK.",
-    [52] = "The server didn't reply anything, which here is considered an error.",
-    [53] = "SSL crypto engine not found.",
-    [54] = "Cannot set SSL crypto engine as default.",
-    [55] = "Failed sending network data.",
-    [56] = "Failure in receiving network data.",
-    [58] = "Problem with the local certificate.",
-    [59] = "Couldn't use specified SSL cipher.",
-    [60] = "Peer certificate cannot be authenticated with known CA certificates.",
-    [61] = "Unrecognized transfer encoding.",
-    [62] = "Invalid LDAP URL.",
-    [63] = "Maximum file size exceeded.",
-    [64] = "Requested FTP SSL level failed.",
-    [65] = "Sending the data requires a rewind that failed.",
-    [66] = "Failed to initialize SSL Engine.",
-    [67] = "The user name, password, or similar was not accepted and curl failed to log in.",
-    [68] = "File not found on TFTP server.",
-    [69] = "Permission problem on TFTP server.",
-    [70] = "Out of disk space on TFTP server.",
-    [71] = "Illegal TFTP operation.",
-    [72] = "Unknown TFTP transfer ID.",
-    [73] = "File already exists (TFTP).",
-    [74] = "No such user (TFTP).",
-    [75] = "Character conversion failed.",
-    [76] = "Character conversion functions required.",
-    [77] = "Problem with reading the SSL CA cert (path? access rights?).",
-    [78] = "The resource referenced in the URL does not exist.",
-    [79] = "An unspecified error occurred during the SSH session.",
-    [80] = "Failed to shut down the SSL connection.",
-    [82] = "Could not load CRL file, missing or wrong format (added in 7.19.0).",
-    [83] = "Issuer check failed (added in 7.19.0).",
-    [84] = "The FTP PRET command failed",
-    [85] = "RTSP: mismatch of CSeq numbers",
-    [86] = "RTSP: mismatch of Session Identifiers",
-    [87] = "unable to parse FTP file list",
-    [88] = "FTP chunk callback reported error",
-    [89] = "No connection available, the session will be queued",
-    [90] = "SSL public key does not matched pinned public key",
-    [91] = "Invalid SSL certificate status.",
-    [92] = "Stream error in HTTP/2 framing layer.",
-  }
-
-  if curl_error_dictionary[code] ~= nil then
-    return "curl error " .. tostring(code) .. ": " .. curl_error_dictionary[code]
-  end
-
-  return "curl error " .. tostring(code) .. ": unknown curl error"
-end
-
-return M
diff --git a/lua/telescope/_extensions/rest.lua b/lua/telescope/_extensions/rest.lua
index c91521d7..a69c3a9a 100644
--- a/lua/telescope/_extensions/rest.lua
+++ b/lua/telescope/_extensions/rest.lua
@@ -4,7 +4,7 @@ if not has_telescope then
   return
 end
 
-local rest = require("rest-nvim")
+local rest_functions = require("rest-nvim.functions")
 
 local state = require("telescope.actions.state")
 
@@ -14,11 +14,9 @@ local finders = require("telescope.finders")
 local pickers = require("telescope.pickers")
 local conf = require("telescope.config").values
 
-local config = require("rest-nvim.config")
-
 local function rest_env_select(_)
-  local pattern = config.get("env_pattern")
-  local edit = config.get("env_edit_command")
+  local pattern = _G._rest_nvim.env_pattern
+  local edit = _G._rest_nvim.env_edit_command
 
   local command = string.format("fd -HI '%s'", pattern)
   local result = io.popen(command):read("*a")
@@ -41,7 +39,7 @@ local function rest_env_select(_)
           if selection == nil then
             return
           end
-          rest.select_env(selection[1])
+          rest_functions.env("set", selection[1])
         end)
         map("i", "<c-o>", function()
           actions.close(prompt_bufnr)
diff --git a/plugin/rest-nvim.lua b/plugin/rest-nvim.lua
new file mode 100644
index 00000000..491d663e
--- /dev/null
+++ b/plugin/rest-nvim.lua
@@ -0,0 +1,69 @@
+if vim.fn.has("nvim-0.9.0") ~= 1 then
+  vim.notify_once("[rest.nvim] rest.nvim requires at least Neovim >= 0.9 in order to work")
+  return
+end
+
+if vim.g.loaded_rest_nvim then
+  return
+end
+
+--- Dependencies management ---
+-------------------------------
+-- This variable is going to hold the dependencies state (whether they are found or not),
+-- to be used later by the `health.lua` module
+local rest_nvim_deps = {}
+
+-- Locate dependencies
+local dependencies = {
+  ["nvim-nio"] = "rest.nvim will not work asynchronously",
+  ["nvim-treesitter"] = "rest.nvim parsing will not work",
+  ["lua-curl"] = "Default HTTP client won't work",
+  xml2lua = "rest.nvim will be completely unable to use XML bodies in your requests",
+  mimetypes = "rest.nvim will be completely unable to recognize the file type of external body files",
+}
+for dep, err in pairs(dependencies) do
+  local found_dep
+  -- Both nvim-nio and lua-curl has a different Lua module name
+  if dep == "nvim-nio" then
+    found_dep = package.searchpath("nio", package.path)
+  elseif dep == "lua-curl" then
+    found_dep = package.searchpath("cURL.safe", package.path)
+  else
+    found_dep = package.searchpath(dep, package.path)
+  end
+
+  -- If the dependency could not be find in the Lua package.path then try to load it using pcall
+  -- in case it has been installed through a regular plugin manager and not rocks.nvim
+  if not found_dep then
+    local found_dep2
+    -- Both nvim-nio and lua-curl has a different Lua module name
+    if dep == "nvim-nio" then
+      found_dep2 = pcall(require, "nio")
+    elseif dep == "lua-curl" then
+      found_dep2 = pcall(require, "cURL.safe")
+    else
+      found_dep2 = pcall(require, dep)
+    end
+
+    rest_nvim_deps[dep] = {
+      found = false,
+      error = err,
+    }
+    if not found_dep2 then
+      vim.notify("[rest.nvim] Dependency '" .. dep .. "' was not found. " .. err, vim.log.levels.ERROR)
+    else
+      rest_nvim_deps[dep] = {
+        found = true,
+        error = err,
+      }
+    end
+  else
+    rest_nvim_deps[dep] = {
+      found = true,
+      error = err,
+    }
+  end
+end
+vim.g.rest_nvim_deps = rest_nvim_deps
+
+vim.g.loaded_rest_nvim = true
diff --git a/plugin/rest-nvim.vim b/plugin/rest-nvim.vim
deleted file mode 100644
index 21a2c32c..00000000
--- a/plugin/rest-nvim.vim
+++ /dev/null
@@ -1,27 +0,0 @@
-if !has('nvim-0.5')
-    echoerr 'Rest.nvim requires at least nvim-0.5. Please update or uninstall'
-    finish
-endif
-
-if exists('g:loaded_rest_nvim') | finish | endif
-
-nnoremap <Plug>RestNvim :lua require('rest-nvim').run()<CR>
-nnoremap <Plug>RestNvimPreview :lua require('rest-nvim').run(true)<CR>
-nnoremap <Plug>RestNvimLast :lua require('rest-nvim').last()<CR>
-" nnoremap <Plug>RestNvimSelectEnv :lua require('rest-nvim').last()<CR>
-
-command! -nargs=? -complete=file RestSelectEnv :lua require('rest-nvim').select_env(<f-args>)<cr>
-
-lua << EOF
-  vim.api.nvim_create_user_command('RestLog', function()
-  vim.cmd(string.format('tabnew %s', vim.fn.stdpath('cache')..'/rest.nvim.log'))
-end, { desc = 'Opens the rest.nvim log.', })
-EOF
-
-let s:save_cpo = &cpo
-set cpo&vim
-
-let &cpo = s:save_cpo
-unlet s:save_cpo
-
-let g:loaded_rest_nvim = 1
diff --git a/rest.nvim-scm-2.rockspec b/rest.nvim-scm-2.rockspec
new file mode 100644
index 00000000..762de9a3
--- /dev/null
+++ b/rest.nvim-scm-2.rockspec
@@ -0,0 +1,46 @@
+local MAJOR, REV = "scm", "-2"
+rockspec_format = "3.0"
+package = "rest.nvim"
+version = MAJOR .. REV
+
+description = {
+  summary = "A fast and asynchronous Neovim HTTP client written in Lua",
+  labels = { "neovim", "rest" },
+  detailed = [[
+    rest.nvim makes use of Lua cURL bindings to make HTTP requests so you don't have to leave Neovim to test your back-end codebase!
+  ]],
+  homepage = "https://github.com/rest-nvim/rest.nvim",
+  license = "GPLv3",
+}
+
+dependencies = {
+  "lua >= 5.1, < 5.4",
+  "nvim-nio",
+  "lua-curl",
+  "mimetypes",
+  "xml2lua",
+}
+
+source = {
+  url = "http://github.com/rest-nvim/rest.nvim/archive/" .. MAJOR .. ".zip",
+  dir = "rest.nvim-" .. MAJOR,
+}
+
+if MAJOR == "scm" then
+  source = {
+    url = "git://github.com/rest-nvim/rest.nvim",
+    branch = "dev",
+  }
+end
+
+build = {
+  type = "builtin",
+  copy_directories = {
+    "doc",
+    "after",
+    "plugin",
+    "syntax",
+    "ftdetect",
+    "ftplugin",
+  }
+}
diff --git a/rest.nvim-scm-3.rockspec b/rest.nvim-scm-3.rockspec
deleted file mode 100644
index 9f432d00..00000000
--- a/rest.nvim-scm-3.rockspec
+++ /dev/null
@@ -1,38 +0,0 @@
-local MAJOR, REV = "scm", "-3"
-rockspec_format = "3.0"
-package = "rest.nvim"
-version = MAJOR .. REV
-
-description = {
-	summary = "A fast Neovim http client written in Lua",
-	labels = { "neovim", "rest"},
-	detailed = [[
-    rest.nvim makes use of a curl wrapper implemented in pure Lua in plenary.nvim so, in other words, rest.nvim is a curl wrapper so you don't have to leave Neovim!
-   ]],
-	homepage = "https://github.com/rest-nvim/rest.nvim",
-	license = "MIT",
-}
-
-dependencies = {
-	"lua >= 5.1, < 5.4",
-    "plenary.nvim",
-}
-
-source = {
-	url = "http://github.com/rest-nvim/rest.nvim/archive/" .. MAJOR .. ".zip",
-	dir = "rest.nvim-" .. MAJOR,
-}
-
-if MAJOR == "scm" then
-	source = {
-		url = "git://github.com/rest-nvim/rest.nvim",
-	}
-end
-
-build = {
-   type = "builtin",
-   copy_directories = {
-       'doc',
-	   'plugin'
-   }
-}
diff --git a/syntax/httpResult.vim b/syntax/httpResult.vim
index 7b644c39..2898ed85 100644
--- a/syntax/httpResult.vim
+++ b/syntax/httpResult.vim
@@ -1,8 +1,8 @@
 if exists("b:current_syntax") | finish | endif
 
 syn match   httpResultComment "\v^#.*$"
-syn keyword httpResultTitle GET POST PATCH PUT HEAD DELETE nextgroup=httpResultPath
-syn match   httpResultPath  /.*$/ contained
+syn keyword httpResultMethod OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT nextgroup=httpResultPath
+syn match   httpResultPath  /.*$/hs=s+1 contained
 
 syn match httpResultField /^\(\w\)[^:]\+:/he=e-1
 syn match httpResultDateField /^[Dd]ate:/he=e-1    nextgroup=httpResultDate
@@ -10,26 +10,28 @@ syn match httpResultDateField /^[Ee]xpires:/he=e-1 nextgroup=httpResultDate
 syn match httpResultDate /.*$/ contained
 
 syn region httpResultHeader start=+^HTTP/+ end=+ + nextgroup=httpResult200,httpResult300,httpResult400,httpResult500
-syn match  httpResult200 /2\d\d.*$/ contained
-syn match  httpResult300 /3\d\d.*$/ contained
-syn match  httpResult400 /4\d\d.*$/ contained
-syn match  httpResult500 /5\d\d.*$/ contained
+syn match  httpResult200 /2\d\d/ nextgroup=httpResultStatus contained
+syn match  httpResult300 /3\d\d/ nextgroup=httpResultstatus contained
+syn match  httpResult400 /4\d\d/ nextgroup=httpResultstatus contained
+syn match  httpResult500 /5\d\d/ nextgroup=httpResultstatus contained
+
+syn match  httpResultStatus /.*$/ contained
 
 syn region httpResultString start=/\vr?"/ end=/\v"/
 syn match  httpResultNumber /\v[ =]@1<=[0-9]*.?[0-9]+[ ,;&\n]/he=e-1
 
-hi link httpResultComment   Comment
-hi link httpResultTitle     Type
-hi link httpResultPath      httpTSURI
-hi link httpResultField     Identifier
-hi link httpResultDateField Identifier
-hi link httpResultDate      String
-hi link httpResultString    String
-hi link httpResultNumber    Number
-hi link httpResultHeader    Type
-hi link httpResult200       String
-hi link httpResult300       Function
-hi link httpResult400       Number
-hi link httpResult500       Number
+hi link httpResultComment   @comment
+hi link httpResultMethod    @type
+hi link httpResultPath      @text.uri
+hi link httpResultField     @constant
+hi link httpResultDateField @constant
+hi link httpResultDate      @attribute
+hi link httpResultString    @string
+hi link httpResultNumber    @number
+hi link httpResultHeader    @constant
+hi link httpResult200       Msg
+hi link httpResult300       MoreMsg
+hi link httpResult400       WarningMsg
+hi link httpResult500       ErrorMsg
 
 let b:current_syntax = "httpResult"
diff --git a/tests/env_vars/post_create_user.http b/tests/env_vars/post_create_user.http
index 1b89860a..ada24aa6 100644
--- a/tests/env_vars/post_create_user.http
+++ b/tests/env_vars/post_create_user.http
@@ -16,7 +16,7 @@ Authorization: Bearer {{TOKEN}}
     "id" : "{{$uuid}}"
 }
 
-----
+# -----
 
 POST {{URL}}
 Content-Type: application/json
diff --git a/tests/post_create_user.http b/tests/post_create_user.http
index 10a69a90..555cb518 100644
--- a/tests/post_create_user.http
+++ b/tests/post_create_user.http
@@ -7,5 +7,5 @@ Content-Type: application/json
     "array": ["a", "b", "c"],
     "object_ugly_closing": {
       "some_key": "some_value"
-  }
+    }
 }
diff --git a/tests/script_vars/script_vars.http b/tests/script_vars/script_vars.http
index 492b65de..f7c3ed7f 100644
--- a/tests/script_vars/script_vars.http
+++ b/tests/script_vars/script_vars.http
@@ -2,14 +2,14 @@
 # Then the second request can be run
 GET https://jsonplaceholder.typicode.com/posts/3
 
-{% 
+--{%
 
 local body = context.json_decode(context.result.body)
 
+-- These environment variables are stored in 'vim.env'
 context.set_env("userId", body.userId)
 context.set_env("postId", body.id)
 
-%}
+--%}
 
-###
 GET https://jsonplaceholder.typicode.com/posts/{{postId}}