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}}