diff --git a/.busted b/.busted new file mode 100644 index 0000000..ed81890 --- /dev/null +++ b/.busted @@ -0,0 +1,13 @@ +return { + _all = { + coverage = false, + lpath = "lua/?.lua;lua/?/init.lua", + lua = "nlua", + }, + default = { + verbose = true, + }, + tests = { + verbose = true, + }, +} diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..bf58eed --- /dev/null +++ b/.luarc.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "runtime": { + "version": "LuaJIT" + }, + "workspace": { + "library": [ + "lua", + "$VIMRUNTIME", + "${3rd}/luassert/library", + "${3rd}/busted/library" + ], + "checkThirdParty": "Disable" + }, + "diagnostics": { + "unusedLocalExclude": [ "_*" ] + } +} diff --git a/README.md b/README.md index 5424ea6..9592f2d 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,6 @@ get a good experience during autocompletion :) ```lua local default_config = { - client = "curl", - env_file = ".env", env_pattern = "\\.env$", env_edit_command = "tabedit", encode_url = true, @@ -176,30 +174,14 @@ local default_config = { }, }, keybinds = { - buffer_local = true, prev = "H", next = "L", }, }, highlight = { enable = true, - timeout = 750, + timeout = 250, }, - ---Example: - --- - ---```lua - ---keybinds = { - --- { - --- "rr", "Rest run", "Run request under the cursor", - --- }, - --- { - --- "rl", "Rest run last", "Re-run latest request", - --- }, - ---} - --- - ---``` - ---@see vim.keymap.set - keybinds = {}, } ``` diff --git a/after/queries/http/highlights.scm b/after/queries/http/highlights.scm index dfb5a5e..894c51e 100644 --- a/after/queries/http/highlights.scm +++ b/after/queries/http/highlights.scm @@ -1,77 +1,77 @@ -; Keywords -(scheme) @module - -; Methods -(method) @function.method - -; Constants -(const_spec) @constant - -; Headers -(header - name: (name) @constant) - -; Variables -(variable_declaration - name: (identifier) @variable) - -(variable_declaration - value: (number) @number) - -(variable_declaration - value: (boolean) @boolean) - -(variable_declaration - value: (string) @string) - -; Fields -(pair - name: (identifier) @variable.member) - -; URL / Host -(host) @string.special.url - -(host - (identifier) @string.special.url) - -(path - (identifier) @string.special.url) - -; Parameters -(query_param - (key) @variable.parameter) - -; Operators -[ - "=" - "?" - "&" - "@" - "<" -] @operator - -; Literals -(target_url) @string.special.url - -(http_version) @constant - -(string) @string - -(number) @number - -(boolean) @boolean - -; Punctuation -[ - "{{" - "}}" -] @punctuation.bracket - -":" @punctuation.delimiter - -; external JSON body -(external_body - file_path: (path) @string.special.path) - -; Comments -(comment) @comment @spell +; ; Keywords +; (scheme) @module +; +; ; Methods +; (method) @function.method +; +; ; Constants +; (const_spec) @constant +; +; ; Headers +; (header +; name: (name) @constant) +; +; ; Variables +; (variable_declaration +; name: (identifier) @variable) +; +; (variable_declaration +; value: (number) @number) +; +; (variable_declaration +; value: (boolean) @boolean) +; +; (variable_declaration +; value: (string) @string) +; +; ; Fields +; (pair +; name: (identifier) @variable.member) +; +; ; URL / Host +; (host) @string.special.url +; +; (host +; (identifier) @string.special.url) +; +; (path +; (identifier) @string.special.url) +; +; ; Parameters +; (query_param +; (key) @variable.parameter) +; +; ; Operators +; [ +; "=" +; "?" +; "&" +; "@" +; "<" +; ] @operator +; +; ; Literals +; (target_url) @string.special.url +; +; (http_version) @constant +; +; (string) @string +; +; (number) @number +; +; (boolean) @boolean +; +; ; Punctuation +; [ +; "{{" +; "}}" +; ] @punctuation.bracket +; +; ":" @punctuation.delimiter +; +; ; external JSON body +; (external_body +; file_path: (path) @string.special.path) +; +; ; Comments +; (comment) @comment @spell diff --git a/after/queries/http/injections.scm b/after/queries/http/injections.scm index 68268af..6ae4b13 100644 --- a/after/queries/http/injections.scm +++ b/after/queries/http/injections.scm @@ -13,5 +13,5 @@ (#set! injection.language "graphql")) ; Lua scripting -((script_variable) @injection.content +((script) @injection.content (#set! injection.language "lua")) diff --git a/ftplugin/http.lua b/ftplugin/http.lua index ea55aa5..1d09f9e 100644 --- a/ftplugin/http.lua +++ b/ftplugin/http.lua @@ -1 +1,4 @@ vim.bo.commentstring = "# %s" + +local commands = require("rest-nvim.commands") +commands.init(0) diff --git a/lua/lualine/components/rest.lua b/lua/lualine/components/rest.lua index 0513668..692064d 100644 --- a/lua/lualine/components/rest.lua +++ b/lua/lualine/components/rest.lua @@ -22,7 +22,7 @@ end function M.update_status() local current_filetype = vim.bo.filetype if current_filetype == "http" then - return _G._rest_nvim.env_file + return vim.b._rest_nvim_env_file end return "" end diff --git a/lua/rest-nvim/api.lua b/lua/rest-nvim/api.lua index 5638d11..fda326e 100644 --- a/lua/rest-nvim/api.lua +++ b/lua/rest-nvim/api.lua @@ -9,14 +9,13 @@ 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" +api.VERSION = "3.0.0" ---rest.nvim namespace used for buffer highlights ---@type number @@ -41,39 +40,4 @@ 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 index f98345e..a6561b8 100644 --- a/lua/rest-nvim/autocmds.lua +++ b/lua/rest-nvim/autocmds.lua @@ -8,38 +8,29 @@ local autocmds = {} -local commands = require("rest-nvim.commands") -local functions = require("rest-nvim.functions") +local result = require("rest-nvim.result") local result_help = require("rest-nvim.result.help") ----Set up Rest autocommands group and set `:Rest` command on `*.http` files +---Set up Rest autocommands group function autocmds.setup() local rest_nvim_augroup = vim.api.nvim_create_augroup("Rest", {}) local keybinds = _G._rest_nvim.result.keybinds - 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", keybinds.prev, function() - functions.cycle_result_pane("prev") + result.cycle_pane(-1) end, { desc = "Go to previous winbar pane", - buffer = keybinds.buffer_local and args.buf or nil, + buffer = args.buf, }) vim.keymap.set("n", keybinds.next, function() - functions.cycle_result_pane("next") + result.cycle_pane(1) end, { desc = "Go to next winbar pane", - buffer = keybinds.buffer_local and args.buf or nil, + buffer = args.buf, }) vim.keymap.set("n", "?", result_help.open, { desc = "Open rest.nvim request results help window", diff --git a/lua/rest-nvim/client/curl.lua b/lua/rest-nvim/client/curl.lua index 2221126..1943ed7 100644 --- a/lua/rest-nvim/client/curl.lua +++ b/lua/rest-nvim/client/curl.lua @@ -12,7 +12,6 @@ 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 @@ -159,156 +158,126 @@ local function get_stats(req, statistics_tbl) 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 - +---return return nil if execution failed +---@param request Request_ Request data to be passed to cURL +---@return table? info The request information (url, method, headers, body, etc) +function client.request_(request) + local info = {} + local logger = assert(_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 + return + end + local host = request.headers["host"] + if host then + request.url = host .. request.url + 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 + -- 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, - }) + -- 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.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 - local query = _url:get_query() - if type(query) == "string" then - _url:set_query("") - for param in vim.gsplit(query, "&") do - _url:set_query(param, curl.U_URLENCODE + curl.U_APPENDQUERY) - end + -- 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.url) + -- Re-add the request query with the encoded parameters + local query = _url:get_query() + if type(query) == "string" then + _url:set_query("") + for param in vim.gsplit(query, "&") do + _url:set_query(param, curl.U_URLENCODE + curl.U_APPENDQUERY) end - -- Re-add the request URL to the req object - req:setopt_url(_url:get_url()) end + -- 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 + -- Set request HTTP version, defaults to HTTP/1.1 + if request.http_version then + local http_version = 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 + -- 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.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 = vim.deepcopy(request.body) + -- local body = vim.deepcopy(request.body) + if request.body then if request.body.__TYPE == "json" then - body.__TYPE = nil - - local json_body_string = vim.json.encode(body) - req:setopt_postfields(json_body_string) + req:setopt_postfields(request.body.data) 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 - + req:setopt_postfields(request.body.data) + elseif request.body.__TYPE == "external" then + local mimetypes = require("mimetypes") + local body_mimetype = mimetypes.guess(request.body.data.path) + local post_data = { + [request.body.data.name and request.body.data.name or "body"] = { + file = request.body.data.path, + type = body_mimetype, + }, + } + req:post(post_data) + elseif request.body.__TYPE == "form" then local form = curl.form() - for k, v in pairs(body) do + for k, v in pairs(request.body.data) do form:add_content(k, v) end req:setopt_httppost(form) + else + logger:error(("'%s' type body is not supported yet"):format(request.body.__TYPE)) + return end + end - -- Request execution - local res_result = {} - local res_headers = {} - req:setopt_writefunction(table.insert, res_result) - req:setopt_headerfunction(table.insert, res_headers) + -- 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 {} + 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 + info.statistics = get_stats(req, stats_config.stats) end - req:close() - end - return ret + info.url = req:getinfo_effective_url() + info.code = req:getinfo_response_code() + info.method = req:getinfo_effective_method() + info.headers = table.concat(res_headers):gsub("\r", "") + info.body = table.concat(res_result) + else + logger:error("Something went wrong when making the request with cURL:\n" .. curl_error(err:no())) + return + end + req:close() + return info end return client diff --git a/lua/rest-nvim/commands.lua b/lua/rest-nvim/commands.lua index 39b28e8..0eef784 100644 --- a/lua/rest-nvim/commands.lua +++ b/lua/rest-nvim/commands.lua @@ -29,77 +29,56 @@ ---@brief ]] ---@class RestCmd ----@field impl fun(args:string[], opts: vim.api.keyset.user_command) The command implementation +---@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") +local dotenv = require("rest-nvim.dotenv") +local request = require("rest-nvim.request") ----@type { [string]: RestCmd } +---@type table local rest_command_tbl = { run = { + -- TODO: run request by name 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 + if #args > 1 then + vim.notify("Running request by name isn't supported yet", vim.log.levels.INFO) + return 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 + request.run() end, }, last = { impl = function(_) - functions.exec("last") + request.run_last() end, }, logs = { impl = function(_) - local logs_path = table.concat({ vim.fn.stdpath("log"), "rest.nvim.log" }, "/") - vim.cmd("tabedit " .. logs_path) + vim.cmd.e(require("rest-nvim.logger").get_logfile()) 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" - ) + if not args[1] or args[1] == "show" then + dotenv.show_registered_file() return + elseif args[1] == "set" then + if #args < 2 then + vim.notify("Not enough arguments were passed to the 'env' command: 2 argument were expected, 1 was passed", vim.log.levels.ERROR) + return + end + dotenv.register_file(args[2]) + elseif args[1] == "select" then + dotenv.select_file() + else + vim.notify("Invalid action '" .. args[1] .. "' provided to 'env' command", vim.log.levels.ERROR) end - - functions.env(args[1], args[2]) end, ---@return string[] complete = function(args) - local actions = { "set", "show" } + local actions = { "show", "set", "select" } if #args < 1 then return actions end @@ -111,7 +90,7 @@ local rest_command_tbl = { -- 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() + return dotenv.find_env_files() end local match = vim.tbl_filter(function(action) @@ -121,42 +100,6 @@ local rest_command_tbl = { 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, }, @@ -168,17 +111,13 @@ local function rest(opts) local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {} local command = rest_command_tbl[cmd] - local logger = _G._rest_nvim.logger + local logger = assert(_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 diff --git a/lua/rest-nvim/config/check.lua b/lua/rest-nvim/config/check.lua index 6928555..f63d57e 100644 --- a/lua/rest-nvim/config/check.lua +++ b/lua/rest-nvim/config/check.lua @@ -23,14 +23,11 @@ end ---@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" }, @@ -62,7 +59,6 @@ function check.validate(cfg) result_keybinds = { cfg.result.keybinds, "table" }, prev = { cfg.result.keybinds.prev, "string" }, next = { cfg.result.keybinds.next, "string" }, - buffer_local = { cfg.result.keybinds.buffer_local, "boolean" }, -- RestConfigHighlight highlight_enable = { cfg.highlight.enable, "boolean" }, timeout = { cfg.highlight.timeout, "number" }, diff --git a/lua/rest-nvim/config/init.lua b/lua/rest-nvim/config/init.lua index c087618..aa5ba31 100644 --- a/lua/rest-nvim/config/init.lua +++ b/lua/rest-nvim/config/init.lua @@ -11,7 +11,7 @@ local config = {} local logger = require("rest-nvim.logger") ---@class RestConfigDebug ----@field unrecognized_configs string[] Unrecognized configuration options +---@field unrecognized_configs table Unrecognized configuration options ---@class RestConfigLogs ---@field level string The logging level name, see `:h vim.log.levels`. Default is `"info"` @@ -48,7 +48,6 @@ local logger = require("rest-nvim.logger") ---@field html string|fun(body: string): string,table HTML formatter ---@class RestConfigResultKeybinds ----@field buffer_local boolean Enable keybinds only in request result buffer ---@field prev string Mapping for cycle to previous result pane ---@field next string Mapping for cycle to next result pane @@ -57,25 +56,20 @@ local logger = require("rest-nvim.logger") ---@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 custom_dynamic_variables table 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 _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, @@ -85,9 +79,6 @@ local default_config = { level = "info", save = true, }, - request = { - pre_script = function() end, - }, result = { split = { horizontal = false, @@ -133,30 +124,14 @@ local default_config = { }, }, keybinds = { - buffer_local = true, prev = "H", next = "L", }, }, highlight = { enable = true, - timeout = 750, + timeout = 250, }, - ---Example: - --- - ---```lua - ---keybinds = { - --- { - --- "rr", ":Rest run", "Run request under the cursor", - --- }, - --- { - --- "rl", ":Rest run last", "Re-run latest request", - --- }, - ---} - --- - ---``` - ---@see vim.keymap.set - keybinds = {}, } ---Set user-defined configurations for rest.nvim @@ -166,7 +141,7 @@ function config.set(user_configs) local check = require("rest-nvim.config.check") local conf = vim.tbl_deep_extend("force", { - debug_info = { + _debug_info = { unrecognized_configs = check.get_unrecognized_keys(user_configs, default_config), }, }, default_config, user_configs) @@ -184,8 +159,8 @@ function config.set(user_configs) 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)) + if #conf._debug_info.unrecognized_configs > 0 then + conf.logger:warn("Unrecognized configs found in setup: " .. vim.inspect(conf._debug_info.unrecognized_configs)) end return conf diff --git a/lua/rest-nvim/context.lua b/lua/rest-nvim/context.lua new file mode 100644 index 0000000..613216b --- /dev/null +++ b/lua/rest-nvim/context.lua @@ -0,0 +1,88 @@ +---@mod rest-nvim.context_ rest.nvim context (mainly contains variables) + +local dotenv = require("rest-nvim.dotenv") +local M = {} + +---@class Context +---@field vars table +---@field files string[] +local Context = {} +Context.__index = Context + +local random = math.random +math.randomseed(os.time()) + +---Generate a random uuid +---@return string +local function uuid() + local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" + local s = string.gsub(template, "[xy]", function(c) + local v = (c == "x") and random(0, 0xf) or random(8, 0xb) + return string.format("%x", v) + end) + return s +end + +---@type table +local rest_variables = { + ["$uuid"] = uuid, + ["$date"] = function() + return os.date("%Y-%m-%d") --[[@as string]] + end, + ["$timestamp"] = function() + return string(os.time()) or "" + end, + ["$randomInt"] = function() + return string(math.random(0, 1000)) + end, +} + +---@return Context +function Context:new() + local obj = { + __index = self, + vars = {}, + files = {}, + } + setmetatable(obj, self) + return obj +end + +---@param filepath string +function Context:load_file(filepath) + table.insert(self.files, filepath) + dotenv.load_file(filepath, function (key, value) + self:set(key, value) + end) +end + +---@param key string +---@param value string +function Context:set(key, value) + self.vars[key] = value +end + +---@param key string +---@return nil|fun():string +local function get_dynamic_vars(key) + local user_variables = _G._rest_nvim.custom_dynamic_variables or {} + return user_variables[key] or rest_variables[key] +end + +---resolves variable +---1. variables from pre-request scripts (local to each requests) +---2. in-place variables (local to each .http files) +---3. selected dotenv file (local to each .http files) +---returns empty string if variable is not set +---@param key string +---@return string value +function Context:resolve(key) + -- find from dynamic variables + local var = get_dynamic_vars(key) + -- find from local variable table or vim.env + return var and var() or self.vars[key] or vim.env[key] or "" +end + +M.Context = Context + +return M diff --git a/lua/rest-nvim/dotenv.lua b/lua/rest-nvim/dotenv.lua new file mode 100644 index 0000000..09710ff --- /dev/null +++ b/lua/rest-nvim/dotenv.lua @@ -0,0 +1,76 @@ +---@mode rest-nvim.dotenv_ dotenv management module + +local M = {} + +local dotenv_parser = require("rest-nvim.parser.dotenv") + +---load dotenv file +---This function will set environment variables in current editor session. +---@see vim.env +---@param path string file path of dotenv file +---@param setter? fun(key:string, value:string) +function M.load_file(path, setter) + vim.validate({ + path = { path, "string" }, + settter = { setter, { "function", "nil" }}, + }) + if not setter then + setter = function (key, value) + vim.env[key] = value + end + end + local ok = dotenv_parser.parse(path, setter) + if not ok then + vim.notify("failed to load file '" .. path .. "'", vim.log.levels.WARN) + end +end + +---register the dotenv file. +---this file will be sourced right before each requests (it won't be sourced +---multiple times when running all requests in current http file) +---@param path string file path of dotenv file +---@param bufnr number? buffer identifier, default to current buffer +function M.register_file(path, bufnr) + -- TODO: validate file extension + bufnr = bufnr or 0 + vim.b[bufnr]._rest_nvim_env_file = path + vim.notify("Env file '" .. path .. "' has been registered") +end + +---show registered dotenv file for current buffer +---@param bufnr number? buffer identifier, default to current buffer +function M.show_registered_file(bufnr) + bufnr = bufnr or 0 + if not vim.b[bufnr]._rest_nvim_env_file then + vim.notify("No env file is used in current buffer", vim.log.levels.WARN) + else + vim.notify("Current env file in use: " .. vim.b._rest_nvim_env_file, vim.log.levels.INFO) + end +end + +---Find a list of environment files starting from the current directory +---@return string[] files Environment variable files path +function M.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 + +---@param bufnr number? buffer identifier, default to current buffer +---@return string? path +function M.select_file(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + vim.ui.select(M.find_env_files(), { + prompt = 'Select env files', + }, function (item, _idx) + M.register_file(item, bufnr) + end) +end + +return M diff --git a/lua/rest-nvim/functions.lua b/lua/rest-nvim/functions.lua deleted file mode 100644 index 3594810..0000000 --- a/lua/rest-nvim/functions.lua +++ /dev/null @@ -1,210 +0,0 @@ ----@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 - -- Load environment variables from the env file before parsing the request - env_vars.read_file(true) - - 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 - -- 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 - -- 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 not (req_results.script == "" or req_results.script == nil) 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 index b34032a..c88d173 100644 --- a/lua/rest-nvim/health.lua +++ b/lua/rest-nvim/health.lua @@ -41,10 +41,6 @@ local function install_health() err_advice = "Install it through your preferred plugins manager or luarocks by using `luarocks --local --lua-version=5.1 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) @@ -52,31 +48,13 @@ local function install_health() 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 + 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") diff --git a/lua/rest-nvim/init.lua b/lua/rest-nvim/init.lua index 2b76bc2..b792213 100644 --- a/lua/rest-nvim/init.lua +++ b/lua/rest-nvim/init.lua @@ -9,7 +9,6 @@ local rest = {} local config = require("rest-nvim.config") -local keybinds = require("rest-nvim.keybinds") local autocmds = require("rest-nvim.autocmds") ---Set up rest.nvim @@ -18,9 +17,6 @@ function rest.setup(user_configs) -- Set up rest.nvim configurations _G._rest_nvim = config.set(user_configs or {}) - -- Set up rest.nvim keybinds - keybinds.apply() - -- Set up rest.nvim autocommands and commands autocmds.setup() end diff --git a/lua/rest-nvim/keybinds.lua b/lua/rest-nvim/keybinds.lua deleted file mode 100644 index 85dc9b6..0000000 --- a/lua/rest-nvim/keybinds.lua +++ /dev/null @@ -1,64 +0,0 @@ ----@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", "RestNvim", function() - vim.deprecate("`RestNvim` mapping", "`:Rest run`", "2.1.0", "rest.nvim", false) - vim.cmd("Rest run") - end) - vim.keymap.set("n", "RestNvimLast", function() - vim.deprecate("`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 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 index e6f49d4..59eddd9 100644 --- a/lua/rest-nvim/logger.lua +++ b/lua/rest-nvim/logger.lua @@ -48,13 +48,22 @@ local default_config = { save_logs = true, } +local default_log_path = vim.fn.stdpath("log") --[[@as string]] + +---Get the rest.nvim log file path. +---@package +---@return string filepath +function logger.get_logfile() + return vim.fs.joinpath(default_log_path, "rest-nvim.log") +end + ---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" }, "/") + local log_path = logger.get_logfile() -- 644 sets read and write permissions for the owner, and it sets read-only -- mode for the group and others diff --git a/lua/rest-nvim/parser/dotenv.lua b/lua/rest-nvim/parser/dotenv.lua new file mode 100644 index 0000000..9a529f8 --- /dev/null +++ b/lua/rest-nvim/parser/dotenv.lua @@ -0,0 +1,85 @@ +local utils = require("rest-nvim.utils") + +local M = {} + +---Get the environment variables file filetype +---@param path string The environment file path +---@return string|nil +local function get_filetype(path) + local ext = vim.fn.fnamemodify(path, ":e") + return ext == "" and nil or ext +end + +---@param value any +---@return string +local function value_tostring(value) + vim.validate({ + value = { + value, + function (v) + return v == vim.NIL or vim.tbl_contains({ "nil", "number", "string", "boolean" }, value) + end, + "vim.NIL|nil|bumber|string|boolean" + } + }) + if value == vim.NIL or value == nil then + return "" + end + return tostring(value) +end + +---parse dotenv file +---with setter, it pass the values to setter function +---when setter isn't provided, returns variables table +---@param path string +---@param setter? fun(key:string,value:string) +---@return boolean ok +---@return table|nil +function M.parse(path, setter) + local logger = assert(_G._rest_nvim.logger) + local vars + if not setter then + vars = {} + setter = function (key, value) + vars[key] = value + end + end + if not utils.file_exists(path) then + logger:error("Current environment file '" .. path .. "' was not found") + return false + end + local env_ext = get_filetype(path) + local file_contents = utils.read_file(path) + if env_ext == "json" then + local ok, json_tbl = pcall(vim.json.decode, file_contents) + if not ok or not type(json_tbl) == "table" or vim.islist(json_tbl) then + logger:error("failed parsing json data") + return false + end + for key, value in pairs(json_tbl) do + if type(key) == "string" and type(value) ~= "table" then + setter(key, value_tostring(value)) + end + end + else + -- FIXME: handle other newline characters + 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 + setter(variable_name, value_tostring(variable_value)) + end + end + return true, vars +end + +return M diff --git a/lua/rest-nvim/parser/dynamic_vars.lua b/lua/rest-nvim/parser/dynamic_vars.lua deleted file mode 100644 index 4bf2cce..0000000 --- a/lua/rest-nvim/parser/dynamic_vars.lua +++ /dev/null @@ -1,60 +0,0 @@ ----@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 deleted file mode 100644 index 5a0d2ca..0000000 --- a/lua/rest-nvim/parser/env_vars.lua +++ /dev/null @@ -1,74 +0,0 @@ ----@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 index ea6bad0..383da11 100644 --- a/lua/rest-nvim/parser/init.lua +++ b/lua/rest-nvim/parser/init.lua @@ -1,448 +1,228 @@ ----@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 ]] +---@mod rest-nvim.pparser rest.nvim tree-sitter parsing module -local parser = {} +local M = {} -local dynamic_vars = require("rest-nvim.parser.dynamic_vars") +local Context = require("rest-nvim.context").Context +local script = require("rest-nvim.script") +local utils = require("rest-nvim.utils") ----@alias NodesList { [string]: TSNode }[] ----@alias Variables { [string]: { type_: string, value: string|number|boolean } }[] +---@alias Source integer|string Buffer or string which the `node` is extracted ----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 +---@alias BodyType "json"|"xml"|"external"|"form"|"graphql" - ---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 = "[" +---@class ReqBody +---@field __TYPE BodyType +---@field data any - 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 +---@param node TSNode +---@param field string +---@param source Source ---@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) +local function get_node_field_text(node, field, source) + local n = node:field(field)[1] + return n and vim.treesitter.get_node_text(n, source) or nil 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 +---@param src string +---@param context Context +---@param encoder? fun(s:string):string +---@return string +---@return integer +local function expand_variables(src, context, encoder) + return src:gsub("{{(.-)}}", function (name) + name = vim.trim(name) + local res = context:resolve(name) + if encoder then + res = encoder(res) end - end - return child_nodes + return res + end) 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) +---@param source Source +---@param context Context +---@return table headers +local function parse_headers(req_node, source, context) 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 + local header_nodes = req_node:field("header") + for _, node in ipairs(header_nodes) do + local key = assert(get_node_field_text(node, "name", source)) + local value = assert(get_node_field_text(node, "value", source)) + key = expand_variables(key, context) + value = expand_variables(value, context) + key = string.lower(key) + headers[key] = value 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)), - } +---@param body_node TSNode +---@param source Source +---@param context Context +---@return ReqBody|nil +function M.parse_body(body_node, source, context) + local logger = assert(_G._rest_nvim.logger) + local body = {} + body.__TYPE = body_node:type():gsub("_%w+", "") + ---@cast body ReqBody + if body.__TYPE == "json" then + body.data = vim.trim(vim.treesitter.get_node_text(body_node, source)) + body.data = expand_variables(body.data, context) + local ok, _ = pcall(vim.json.decode, body.data) + if not ok then + logger:warn("invalid json: '" .. body.data .. "'") + return nil 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 + elseif body.__TYPE == "xml" then + body.data = vim.trim(vim.treesitter.get_node_text(body_node, source)) + body.data = expand_variables(body.data, context) + local xml2lua = require("xml2lua") + local handler = require("xmlhandler.tree"):new() + local parser = xml2lua.parser(handler) + local ok = pcall(function (t) return parser:parse(t) end, body.data) + if not ok then + logger:warn("invalid xml: '" .. body.data .. "'") + return nil 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('"', "") + elseif body.__TYPE == "form" then + body.data = {} + for pair, _ in body_node:iter_children() do + if pair:type() == "query" then + local key = assert(get_node_field_text(pair, "key", source)) + local value = assert(get_node_field_text(pair, "value", source)) + key = expand_variables(key, context) + value = expand_variables(value, context) + body.data[key] = value end end - text = text:gsub("{{[%s]?" .. variable_name .. "[%s]?}}", variable_value) + elseif body.__TYPE == "external" then + body.data = { + name = assert(get_node_field_text(body_node, "name", source)), + path = assert(get_node_field_text(body_node, "path", source)), + } + elseif body.__TYPE == "graphql" then + logger:error("graphql body is not supported yet") end - return text + return body 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/", "") - elseif node_type == "request" then - request = parser.parse_request(traverse_request(node), variables) +---parse all in-place variables from source +---@param source Source +---@return Context ctx +function M.create_context(source) + local ctx = Context:new() + local _, tree = utils.ts_parse_source(source) + for node, _ in tree:root():iter_children() do + if node:type() == "variable_declaration" then + M.parse_variable_declaration(node, source, ctx) 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 + return ctx 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 +---@return TSNode? +function M.get_cursor_request_node() + local node = vim.treesitter.get_node() + return node and utils.ts_find(node, "request") 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 +---@return TSNode[] +function M.get_all_request_node() + local source = 0 + local _, tree = utils.ts_parse_source(source) + local reqs = {} + for node, _ in tree:root():iter_children() do + if node:type() == "request" then + table.insert(reqs, node) 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 + return reqs 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) - 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 +---@param vd_node TSNode +---@param source Source +---@param ctx Context +function M.parse_variable_declaration(vd_node, source, ctx) + local name = assert(get_node_field_text(vd_node, "name", source)) + local value = vim.trim(assert(get_node_field_text(vd_node, "value", source))) + value = expand_variables(value, ctx) + ctx:set(name, value) 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)) +---@param node TSNode +---@param source Source +---@param context Context +---@return function +function M.parse_script(node, source, context) + vim.validate({ node = utils.ts_node_spec(node, "script") }) + local str = vim.treesitter.get_node_text(node, source):sub(3,-3) + return script.load(str, context) end ----@class RequestReq ----@field method string The request method ----@field url string The request URL ----@field http_version? string The request HTTP protocol +---@param script_node TSNode +---@param source Source +---@param context Context +function M.parse_pre_request_script(script_node, source, context) + local node = assert(script_node:named_child(0)) + M.parse_script(node, source, context)() +end ----@class Request ----@field request RequestReq ----@field headers { [string]: string|number|boolean }[] ----@field body table ----@field script? string ----@field start number ----@field end_ number +---@param handler_node TSNode +---@param source Source +---@param context Context +function M.parse_request_handler(handler_node, source, context) + local node = assert(handler_node:named_child(0)) + return M.parse_script(node, source, context) +end ----Parse a request and return the request on itself, its headers and body +---Parse the request node and create Request object. Returns `nil` if parsing +---failed. ---@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 = "", +---@param source Source +---@param context? Context +---@return Request_|nil +function M.parse(req_node, source, context) + context = context or Context:new() + local logger = assert(_G._rest_nvim.logger) + -- request should not include error + if req_node:has_error() then + logger:error(utils.ts_node_error_log(req_node)) + return nil + end + local body_node = req_node:field("body")[1] + local body = body_node and M.parse_body(body_node, source, context) + if body_node and not body then + logger:error("parsing body failed") + return nil + end + local method = get_node_field_text(req_node, "method", source) + if not method then + logger:info("no method provided, falling back to 'GET'") + method = "GET" + end + local url = expand_variables( + assert(get_node_field_text(req_node, "url", source)), + context, + _G._rest_nvim.encode_url and utils.escape or nil + ) + local pre_req_scripts = req_node:field("pre_request_script") + for _, script_node in ipairs(pre_req_scripts) do + M.parse_pre_request_script(script_node, source, context) + end + local handlers = vim.iter(req_node:field("handler_script")):map(function (node) + return M.parse_request_handler(node, source, context) + end):totable() + ---@type Request_ + return { + context = context, + method = method, + url = url, + http_version = get_node_field_text(req_node, "version", source), + headers = parse_headers(req_node, source, context), + body = body, + handlers = handlers, } - 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 +return M diff --git a/lua/rest-nvim/parser/script_vars.lua b/lua/rest-nvim/parser/script_vars.lua deleted file mode 100644 index 777c4b9..0000000 --- a/lua/rest-nvim/parser/script_vars.lua +++ /dev/null @@ -1,32 +0,0 @@ ----@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.lua b/lua/rest-nvim/request.lua new file mode 100644 index 0000000..4c67ca2 --- /dev/null +++ b/lua/rest-nvim/request.lua @@ -0,0 +1,103 @@ +---@mod rest-nvim.request_ rest.nvim request APIs + +local M = {} + +local parser = require("rest-nvim.parser") +local utils = require("rest-nvim.utils") + +---@class Request_ +---@field context Context +---@field name? string The request identifier +---@field method string The request method +---@field url string The request URL +---@field http_version? string The request HTTP protocol +---@field headers table +---@field body? ReqBody +---@field handlers fun()[] + +---@type Request_|nil +_G._rest_nvim_last_request_ = nil + +---@param req Request_ +---@return boolean ok +local function run_request(req) + local logger = assert(_G._rest_nvim.logger) + local client = require("rest-nvim.client.curl") + _G._rest_nvim_last_request_ = req + + local res = client.request_(req) + if not res then + logger:error("request failed") + return false + end + + -- run request handler scripts + -- TODO: run them with response info + vim.iter(req.handlers):each(function (f) f() end) + + -- update result UI + local result = require("rest-nvim.result") + local result_buf = result.get_or_create_buf() + result.write_res(result_buf, res) + return true +end + +---run request in current cursor position +---@return boolean ok +function M.run() + local logger = assert(_G._rest_nvim.logger) + local req_node = parser.get_cursor_request_node() + if not req_node then + logger:error("failed to find request at cursor position") + return false + end + local ctx = parser.create_context(0) + if vim.b._rest_nvim_env_file then + ctx:load_file(vim.b._rest_nvim_env_file) + end + local req = parser.parse(req_node, 0, ctx) + if not req then + logger:error("failed to parse request") + return false + end + local highlight = _G._rest_nvim.highlight + if highlight.enable then + utils.ts_highlight_node(0, req_node, require("rest-nvim.api").namespace) + end + return run_request(req) +end + +---run last request +---@return boolean ok +function M.run_last() + local logger = assert(_G._rest_nvim.logger) + local req = _G._rest_nvim_last_request_ + if not req then + logger:warn("no last request") + return false + end + return run_request(req) +end + +---run all requests in current file with same context +---@return boolean ok +function M.run_all() + local logger = assert(_G._rest_nvim.logger) + logger:error("not implemented") + local reqs = parser.get_all_request_node() + local ctx = parser.create_context(0) + for _, req_node in ipairs(reqs) do + local req = parser.parse(req_node, 0, ctx) + if not req then + logger:error("failed to run request") + return false + end + local ok = run_request(req) + if not ok then + return false + end + end + return true +end + +return M diff --git a/lua/rest-nvim/result/init.lua b/lua/rest-nvim/result/init.lua index 1585c05..314442c 100644 --- a/lua/rest-nvim/result/init.lua +++ b/lua/rest-nvim/result/init.lua @@ -21,13 +21,19 @@ result.bufnr = nil ---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) +function result.select_pane(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 +---@param count number +function result.cycle_pane(count) + local idx = winbar.current_pane_index + result.select_pane(idx + count) +end + ---Move the cursor to the desired position in the given buffer ---@param bufnr number Buffer handler number ---@param row number The desired line @@ -154,7 +160,7 @@ function result.display_buf(bufnr, stats) end -- Disable concealing for the results buffer window - vim.api.nvim_set_option_value("conceallevel", 0, { win = winnr }) + -- 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 }) @@ -239,6 +245,7 @@ local function format_body(bufnr, headers, res) end body = vim.split(res.body, "\n") + -- TODO: apply result.behavior.decode_url option table.insert(body, 1, res.method .. " " .. res.url) table.insert(body, 2, headers[1]) -- HTTP/X and status code + meaning table.insert(body, 3, "") diff --git a/lua/rest-nvim/script.lua b/lua/rest-nvim/script.lua new file mode 100644 index 0000000..68dc9a8 --- /dev/null +++ b/lua/rest-nvim/script.lua @@ -0,0 +1,70 @@ +---@mod rest-nvim.sscript rest.nvim tree-sitter parsing module + +local M = {} + +-- TODO: fill script environment + +---@param ctx Context +---@return ScriptEnv +function M.create_env(ctx) + ---@class ScriptEnv + local env = { + ---@class ScriptEnvClient + client = { + test = function () end, + assert = function () end, + ---@class ScriptEnvClientGlobal + global = { + ---@param key string + ---@param value string + set = function (key, value) + vim.env[key] = value + end, + ---@param key string + ---@return string value + get = function (key) + return ctx:resolve(key) + end, + }, + }, + ---@class ScriptEnvRequest + request = { + body = {}, + environment = {}, + headers = {}, + method = "GET", + url = "", + ---@class ScriptEnvRequestVariables + variables = { + ---sets request-local variable + ---@param key string + ---@param value string + set = function (key, value) + ctx:set(key, value) + end, + } + }, + ---@class ScriptEnvResponse + response = nil, + -- body + -- headers + -- status + -- content_type + } + return env +end + +---load script for current context +---@param script string +---@return function +function M.load(script, context) + local logger = assert(_G._rest_nvim.logger) + local env = M.create_env(context) + local f, error_msg = load(script, "script_variable", "bt", env) + if error_msg then + logger:error(error_msg) + end + return assert(f) +end + +return M diff --git a/lua/rest-nvim/utils.lua b/lua/rest-nvim/utils.lua index ad3ddd3..288cd6e 100644 --- a/lua/rest-nvim/utils.lua +++ b/lua/rest-nvim/utils.lua @@ -138,8 +138,7 @@ function utils.highlight(bufnr, start, end_, ns) ns, higroup, { start, 0 }, - { end_, string.len(vim.fn.getline(end_)) }, - { regtype = "c", inclusive = false } + { end_, string.len(vim.fn.getline(end_)) } ) -- Clear buffer highlights again after timeout @@ -150,4 +149,89 @@ function utils.highlight(bufnr, start, end_, ns) end, timeout) end +---@param bufnr number +---@param node TSNode +---@param ns number +function utils.ts_highlight_node(bufnr, node, ns) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local highlight = _G._rest_nvim.highlight + local higroup = "IncSearch" + local s_row, s_col = node:start() + local e_row, e_col = node:end_() + vim.highlight.range( + bufnr, + ns, + higroup, + { s_row, s_col }, + { e_row, e_col }, + { regtype = "v" } + ) + + -- 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, highlight.timeout) +end + +---@param source string|integer +---@return vim.treesitter.LanguageTree +---@return TSTree +function utils.ts_parse_source(source) + local ts_parser + if type(source) == "string" then + ts_parser = vim.treesitter.get_string_parser(source, "http") + else + ts_parser = vim.treesitter.get_parser(source, "http") + end + return ts_parser, assert(ts_parser:parse({})[1]) +end + +---@param node TSNode +---@param type string +---@return TSNode? +function utils.ts_find(node, type) + if node:type() == type then + return node + end + local parent = node:parent() + if parent then + return utils.ts_find(parent, type) + end + return nil +end + +---@param node TSNode +---@param expected_type string +---@return table +function utils.ts_node_spec(node, expected_type) + return { + node, + function (n) + return n:type() == expected_type + end, + "("..expected_type..") TSNode", + } +end + +---Create error log for TSNode that has a syntax error +---@param node TSNode Tree-sitter node +---@return string +function utils.ts_node_error_log(node) + local s_row, s_col = node:start() + local e_row, e_col = node: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 "The tree-sitter node at the range " .. range .. " has a syntax error and cannot be parsed" +end + return utils diff --git a/lua/telescope/_extensions/rest.lua b/lua/telescope/_extensions/rest.lua index a69c3a9..411ac1f 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_functions = require("rest-nvim.functions") +local dotenv = require("rest-nvim.dotenv") local state = require("telescope.actions.state") @@ -18,6 +18,7 @@ local function rest_env_select(_) local pattern = _G._rest_nvim.env_pattern local edit = _G._rest_nvim.env_edit_command + -- TODO: use dotenv.find_env_files instead local command = string.format("fd -HI '%s'", pattern) local result = io.popen(command):read("*a") @@ -39,7 +40,7 @@ local function rest_env_select(_) if selection == nil then return end - rest_functions.env("set", selection[1]) + dotenv.register_file(selection[1]) end) map("i", "", function() actions.close(prompt_bufnr) diff --git a/plugin/rest-nvim.lua b/plugin/rest-nvim.lua index 1897f5e..f777f46 100644 --- a/plugin/rest-nvim.lua +++ b/plugin/rest-nvim.lua @@ -16,7 +16,6 @@ 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", diff --git a/rest.nvim-scm-1.rockspec b/rest.nvim-scm-1.rockspec index f649dc6..5c1fc79 100644 --- a/rest.nvim-scm-1.rockspec +++ b/rest.nvim-scm-1.rockspec @@ -21,6 +21,15 @@ dependencies = { "xml2lua", } +test_dependencies = { + "lua >= 5.1, < 5.4", + "nvim-nio", + "lua-curl", + "mimetypes", + "xml2lua", + "nlua", +} + source = { url = "http://github.com/rest-nvim/rest.nvim/archive/" .. MAJOR .. ".zip", dir = "rest.nvim-" .. MAJOR,