diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 1e75b9c0..77783097 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -7,8 +7,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v18 + - uses: cachix/install-nix-action@v21 with: nix_path: nixpkgs=channel:nixos-unstable - run: | - nix develop ./contrib -c make test + nix develop .#ci -c make test diff --git a/README.md b/README.md index 024f74e4..88529dd3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ have to leave Neovim! ## 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, @@ -77,6 +78,9 @@ use { 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, -- executables or functions for formatting response body [optional] @@ -149,6 +153,13 @@ request method (e.g. `GET`) and run `rest.nvim`. --- +### Debug + + +Run `export DEBUG_PLENARY="debug"` before starting nvim. Logs will appear most +likely in ~/.cache/nvim/rest.nvim.log + + ## Contribute 1. Fork it (https://github.com/rest-nvim/rest.nvim/fork) @@ -162,6 +173,7 @@ test`. ## Related software +- [vim-rest-console](https://github.com/diepm/vim-rest-console) - [Hurl](https://hurl.dev/) - [HTTPie](https://httpie.io/) - [httpYac](https://httpyac.github.io/) diff --git a/doc/rest-nvim.txt b/doc/rest-nvim.txt index efc8dcbc..ce7d9132 100644 --- a/doc/rest-nvim.txt +++ b/doc/rest-nvim.txt @@ -21,6 +21,7 @@ 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| 5. Known issues..........................................|rest-nvim-issues| 6. License..............................................|rest-nvim-license| 7. Contributing....................................|rest-nvim-contributing| @@ -119,6 +120,9 @@ COMMANDS *rest-nvim-usage-commands* 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. @@ -220,6 +224,22 @@ You can extend or overwrite built-in dynamic variables, with the config key ` },` `})` +=============================================================================== +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 + +vim.api.nvim_create_autocmd("User", { +pattern = "RestStartRequest", +once = true, + callback = function(opts) + print("IT STARTED") + vim.pretty_print(opts) + end, +}) + =============================================================================== KNOWN ISSUES *rest-nvim-issues* diff --git a/contrib/flake.lock b/flake.lock similarity index 100% rename from contrib/flake.lock rename to flake.lock diff --git a/contrib/flake.nix b/flake.nix similarity index 65% rename from contrib/flake.nix rename to flake.nix index 3587bbc7..84538380 100644 --- a/contrib/flake.nix +++ b/flake.nix @@ -13,40 +13,11 @@ mkDevShell = luaVersion: let - # luaPkgs = pkgs."lua${luaVersion}".pkgs; luaEnv = pkgs."lua${luaVersion}".withPackages (lp: with lp; [ + busted luacheck luarocks ]); - neovimConfig = pkgs.neovimUtils.makeNeovimConfig { - plugins = with pkgs.vimPlugins; [ - { - plugin = packer-nvim; - type = "lua"; - config = '' - require('packer').init({ - luarocks = { - python_cmd = 'python' -- Set the python command to use for running hererocks - }, - }) - -- require my own manual config - require('init-manual') - ''; - } - { 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 pkgs.mkShell { name = "rest-nvim"; @@ -54,8 +25,6 @@ pkgs.sumneko-lua-language-server luaEnv pkgs.stylua - myNeovim - # pkgs.neovim # assume user has one already installed ]; shellHook = let @@ -67,30 +36,45 @@ tree-sitter-json ] ))]; - # opt = map (x: x.plugin) pluginsPartitioned.right; }; - # }; packDirArgs.myNeovimPackages = myVimPackage; in '' - cat <<-EOF > minimal.vim - set rtp+=. - set packpath^=${pkgs.vimUtils.packDir packDirArgs} - EOF + export DEBUG_PLENARY="debug" + cat <<-EOF > minimal.vim + set rtp+=. + set packpath^=${pkgs.vimUtils.packDir packDirArgs} + EOF ''; }; in { - # packages = { - # default = self.packages.${system}.luarocks-51; - # luarocks-51 = mkPackage "5_1"; - # luarocks-52 = mkPackage "5_2"; - # }; - 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/ftplugin/http.vim b/ftplugin/http.vim new file mode 100644 index 00000000..9c2e80a0 --- /dev/null +++ b/ftplugin/http.vim @@ -0,0 +1 @@ +set commentstring=#\ %s diff --git a/lua/rest-nvim/config/init.lua b/lua/rest-nvim/config/init.lua index fdc754fd..d78d2e77 100644 --- a/lua/rest-nvim/config/init.lua +++ b/lua/rest-nvim/config/init.lua @@ -10,6 +10,7 @@ local config = { timeout = 150, }, result = { + show_curl_command = true, show_url = true, show_http_info = true, show_headers = true, diff --git a/lua/rest-nvim/curl/init.lua b/lua/rest-nvim/curl/init.lua index 00b93bb7..fe1a624c 100644 --- a/lua/rest-nvim/curl/init.lua +++ b/lua/rest-nvim/curl/init.lua @@ -15,6 +15,22 @@ local function is_executable(x) 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 + -- 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() @@ -24,9 +40,9 @@ M.get_or_create_buf = function() local existing_bufnr = vim.fn.bufnr(tmp_name) if existing_bufnr ~= -1 then -- Set modifiable - vim.api.nvim_buf_set_option(existing_bufnr, "modifiable", true) + vim.api.nvim_set_option_value("modifiable", true, { buf = existing_bufnr }) -- Prevent modified flag - vim.api.nvim_buf_set_option(existing_bufnr, "buftype", "nofile") + vim.api.nvim_set_option_value("buftype", "nofile", { buf = existing_bufnr }) -- Delete buffer content vim.api.nvim_buf_set_lines( existing_bufnr, @@ -37,21 +53,21 @@ M.get_or_create_buf = function() ) -- Make sure the filetype of the buffer is httpResult so it will be highlighted - vim.api.nvim_buf_set_option(existing_bufnr, "ft", "httpResult") + 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, "nomodeline") + local new_bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_name(new_bufnr, tmp_name) - vim.api.nvim_buf_set_option(new_bufnr, "ft", "httpResult") - vim.api.nvim_buf_set_option(new_bufnr, "buftype", "nofile") + 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(method, url, script_str) +local function create_callback(curl_cmd, method, url, script_str, req_var) return function(res) if res.exit ~= 0 then log.error("[rest.nvim] " .. utils.curl_error(res.exit)) @@ -63,7 +79,7 @@ local function create_callback(method, url, script_str) -- get content type for _, header in ipairs(res.headers) do if string.lower(header):find("^content%-type") then - content_type = header:match("application/(%l+)") or header:match("text/(%l+)") + content_type = header:match("application/([-a-z]+)") or header:match("text/(%l+)") break end end @@ -83,6 +99,11 @@ local function create_callback(method, url, script_str) end end + -- This can be quite verbose so let user control it + if config.get("result").show_curl_command then + vim.api.nvim_buf_set_lines(res_bufnr, 0, 0, false, { "Command: " .. curl_cmd }) + end + if config.get("result").show_url then --- Add metadata into the created buffer (status code, date, etc) -- Request statement (METHOD URL) @@ -148,11 +169,24 @@ local function create_callback(method, url, script_str) }, false, {}) end end + -- check if the response is a json + -- parse the json response and store the data on memory + if content_type == "json" and req_var ~= "" then + local req_var_store = vim.api.nvim_get_var("req_var_store") + req_var_store[req_var] = vim.json.decode(res.body) + vim.api.nvim_set_var("req_var_store", req_var_store) + end -- append response container - res.body = "#+RESPONSE\n" .. res.body .. "\n#+END" + 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(res.body, "\n") + local lines = utils.split(buf_content, "\n") local line_count = vim.api.nvim_buf_line_count(res_bufnr) - 1 vim.api.nvim_buf_set_lines(res_bufnr, line_count, line_count + #lines, false, lines) @@ -167,7 +201,7 @@ local function create_callback(method, url, script_str) end vim.cmd(cmd_split .. res_bufnr) -- Set unmodifiable state - vim.api.nvim_buf_set_option(res_bufnr, "modifiable", false) + vim.api.nvim_set_option_value("modifiable", false, { buf = res_bufnr }) end -- Send cursor in response buffer to start @@ -194,32 +228,19 @@ local function create_callback(method, url, script_str) end 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 - -- 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) - if opts.dry_run then - local res = curl[opts.method](opts) - local curl_cmd = format_curl_cmd(res) + -- 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) + if opts.dry_run then if config.get("yank_dry_run") then vim.cmd("let @+=" .. string.format("%q", curl_cmd)) end @@ -227,7 +248,9 @@ M.curl_cmd = function(opts) vim.api.nvim_echo({ { "[rest.nvim] Request preview:\n", "Comment" }, { curl_cmd } }, false, {}) return else - opts.callback = vim.schedule_wrap(create_callback(opts.method, opts.url, opts.script_str)) + opts.callback = vim.schedule_wrap( + create_callback(curl_cmd, opts.method, opts.url, opts.script_str, opts.req_var) + ) curl[opts.method](opts) end end diff --git a/lua/rest-nvim/init.lua b/lua/rest-nvim/init.lua index bc18112e..0ca3f8c7 100644 --- a/lua/rest-nvim/init.lua +++ b/lua/rest-nvim/init.lua @@ -1,22 +1,33 @@ -local request = require("rest-nvim.request") +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") local rest = {} local Opts = {} +local defaultRequestOpts = { + verbose = false, + highlight = false, +} + local LastOpts = {} +vim.api.nvim_set_var("req_var_store", { __loaded = true }) 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 = request.get_current_request() + 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 @@ -29,51 +40,143 @@ end -- @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) - local new_buf = vim.api.nvim_create_buf(false, false) + opts = vim.tbl_deep_extend( + "force", -- use value from rightmost map + defaultRequestOpts, + 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 last_line = vim.fn.line("$") - - -- reset cursor position - vim.fn.cursor(1, 1) - local curpos = vim.fn.getcurpos() - while curpos[2] <= last_line do - local ok, req = request.buf_get_request(new_buf, curpos) - if ok then - -- request.print_request(req) - curpos[2] = req.end_line + 1 - rest.run_request(req, opts) + + 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 = "" + for key, val in pairs(headers) do + if string.lower(key) == "content-type" then + content_type = val + break + end + end + 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 - return false, req + -- 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 true 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 - { verbose = false }, -- defaults + defaultRequestOpts, 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 + Opts = { method = result.method:lower(), url = result.url, -- plenary.curl can't set http protocol version -- http_version = result.http_version, - headers = result.headers, - raw = config.get("skip_ssl_verification") and vim.list_extend(result.raw, { "-k" }) - or result.raw, - body = result.body, + 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, + req_var = result.req_var, script_str = result.script_str, } @@ -81,11 +184,27 @@ rest.run_request = function(req, opts) LastOpts = Opts end - if config.get("highlight").enabled then - request.highlight(result.bufnr, result.start_line, result.end_line) + if opts.highlight then + backend.highlight(result.bufnr, result.start_line, result.end_line) end + local request_id = vim.loop.now() + local data = { + requestId = request_id, + request = req, + } + + vim.api.nvim_exec_autocmds("User", { + pattern = "RestStartRequest", + modeline = false, + data = data, + }) local success_req, req_err = pcall(curl.curl_cmd, Opts) + vim.api.nvim_exec_autocmds("User", { + pattern = "RestStopRequest", + modeline = false, + data = vim.tbl_extend("keep", { status = success_req, message = req_err }, data), + }) if not success_req then vim.api.nvim_err_writeln( @@ -104,7 +223,7 @@ rest.last = function() end if config.get("highlight").enabled then - request.highlight(LastOpts.bufnr, LastOpts.start_line, LastOpts.end_line) + backend.highlight(LastOpts.bufnr, LastOpts.start_line, LastOpts.end_line) end local success_req, req_err = pcall(curl.curl_cmd, LastOpts) @@ -117,12 +236,12 @@ rest.last = function() end end -rest.request = request +rest.request = backend -rest.select_env = function(path) +rest.select_env = function(env_file) if path ~= nil then - vim.validate({ path = { path, "string" } }) - config.set({ env_file = path }) + vim.validate({ env_file = { env_file, "string" } }) + config.set({ env_file = env_file }) else print("No path given") end diff --git a/lua/rest-nvim/request/init.lua b/lua/rest-nvim/request/init.lua index 53bb5c16..7854c928 100644 --- a/lua/rest-nvim/request/init.lua +++ b/lua/rest-nvim/request/init.lua @@ -1,11 +1,11 @@ local utils = require("rest-nvim.utils") -local path = require("plenary.path") 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() @@ -18,18 +18,13 @@ local function get_importfile_name(bufnr, start_line, stop_line) if import_line > 0 then local fileimport_string local fileimport_line - local fileimport_spliced + 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+$", "") - 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 + string.gsub(fileimport_line[1], "<@?", "", 1):gsub("^%s+", ""):gsub("%s+$", "") + return fileimport_inlined, fileimport_string end return nil end @@ -41,26 +36,22 @@ end -- @param bufnr Buffer number, a.k.a id -- @param start_line Line where body starts -- @param stop_line Line where body stops --- @param has_json True if content-type is set to json -local function get_body(bufnr, start_line, stop_line, has_json) +-- @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 importfile = get_importfile_name(bufnr, start_line, stop_line) - local lines + local inline, importfile = get_importfile_name(bufnr, start_line, stop_line) + local lines -- an array of strings if importfile ~= nil then - if not utils.file_exists(importfile) then - error("import file " .. importfile .. " not found") - end - lines = utils.read_file(importfile) + return { external = true, inline = inline, filename_tpl = importfile } else - lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, stop_line, false) + lines = vim.api.nvim_buf_get_lines(bufnr, start_line, stop_line, false) end - 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 + local lines2 = {} for _, line in ipairs(lines) do -- stop if a script opening tag is found if line:find("{%%") then @@ -68,29 +59,11 @@ local function get_body(bufnr, start_line, stop_line, has_json) end -- Ignore commented lines with and without indent if not utils.contains_comments(line) then - body = body .. utils.replace_vars(line, vars) - end - end - - local is_json, json_body = pcall(vim.json.decode, body) - - if is_json 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) + lines2[#lines2 + 1] = line end end - - return body + return { external = false, inline = false, body_tpl = lines2 } end local function get_response_script(bufnr, start_line, stop_line) @@ -162,7 +135,7 @@ local function get_headers(bufnr, start_line, end_line) local header_name, header_value = line_content:match("^(.-): ?(.*)$") if not utils.contains_comments(header_name) then - headers[header_name] = utils.replace_vars(header_value) + headers[header_name] = header_value end ::continue:: end @@ -178,6 +151,7 @@ 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] @@ -226,6 +200,19 @@ local function start_request(bufnr, linenumber) return res end +-- request_var will find the request variable if it exists, e.g. +-- ``` +-- #@foo +-- GET http://localhost:8081/bar +-- ```` +-- of the current request and returns the linenumber of the found request variable +-- the request variable is defined as the variable that starts with '#@' +-- one line above the request line in the example the variable would be 'foo' +-- @param url_line The request line +local function request_var(url_line) + return vim.fn.search("^#@", "cbn", url_line - 1) +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 nummer of the .http-file @@ -240,8 +227,8 @@ local function end_request(bufnr, linenumber) end utils.move_cursor(bufnr, linenumber) - local next = vim.fn.search("^GET\\|^POST\\|^PUT\\|^PATCH\\|^DELETE\\|^###\\", "cn", - vim.fn.line("$")) + local next = + vim.fn.search("^GET\\|^POST\\|^PUT\\|^PATCH\\|^DELETE\\|^###\\", "cn", vim.fn.line("$")) -- restore cursor position utils.move_cursor(bufnr, oldlinenumber) @@ -286,6 +273,14 @@ local function parse_url(stmt) } end +-- parse_req_var returns a string with the name of the request variable, e.g. +-- #@foo -> parse_req_var -> foo +-- @param stmt the request variable (#@foo) +local function parse_req_var(stmt) + local parsed = stmt:sub(3) + return parsed +end + local M = {} M.get_current_request = function() return M.buf_get_request(vim.api.nvim_win_get_buf(0), vim.fn.getcurpos()) @@ -308,6 +303,12 @@ M.buf_get_request = function(bufnr, curpos) local parsed_url = parse_url(vim.fn.getline(start_line)) + local req_var_line = request_var(start_line) + local parsed_req_var_str = "" + if req_var_line ~= 0 then + parsed_req_var_str = parse_req_var(vim.fn.getline(req_var_line)) + end + local headers, headers_end = get_headers(bufnr, start_line, end_line) local curl_args, body_start = get_curl_args(bufnr, headers_end, end_line) @@ -319,54 +320,93 @@ M.buf_get_request = function(bufnr, curpos) headers["host"] = nil end - local content_type = "" - - for key, val in pairs(headers) do - if string.lower(key) == "content-type" then - content_type = val - break - end - end - - local body = get_body( - bufnr, - body_start, - end_line, - content_type:find("application/[^ ]*json") - ) + 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 - return true, - { - 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 - } + 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, + req_var = parsed_req_var_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 = [[ - version: ]] .. req.url .. [[\n + url : ]] .. req.url .. [[\n method: ]] .. req.method .. [[\n - start_line: ]] .. tostring(req.start_line) .. [[\n - end_line: ]] .. tostring(req.end_line) .. [[\n - ]] - print(str) + 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") @@ -385,8 +425,7 @@ M.highlight = function(bufnr, start_line, end_line) higroup, { start_line - 1, 0 }, { end_line - 1, end_column }, - "c", - false + { regtype = "c", inclusive = false } ) vim.defer_fn(function() diff --git a/lua/rest-nvim/utils/init.lua b/lua/rest-nvim/utils/init.lua index e2c3c45f..e11014bb 100644 --- a/lua/rest-nvim/utils/init.lua +++ b/lua/rest-nvim/utils/init.lua @@ -5,6 +5,14 @@ math.randomseed(os.time()) local M = {} +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 @@ -207,10 +215,13 @@ M.read_document_variables = function() 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) + -- check if variable has assigned other variable, e.g. foo: {{bar}} + local value_processed = M.replace_req_varibles(M.get_node_value(value, bufnr)) + variables[M.get_node_value(name, bufnr)] = value_processed elseif type ~= "comment" then break end @@ -226,6 +237,51 @@ M.read_variables = function() return vim.tbl_extend("force", first, second, third) end +-- replaces the variables that have assigned another variable, e.g. foo: {{bar}} or foo: {{bar.baz}} +-- if so, then replace {{bar}} or {{bar.baz}} with the proper value else return the same string +-- only works if `bar` is a key in req_var_store +-- @param value_str the value to evaluate +M.replace_req_varibles = function(value_str) + -- first check if 'value_str' has the form {{bar}} if not then return them as is + local match = string.match(value_str, "{{[^}]+}}") + if match == nil then + return value_str + end + + match = match:gsub("{", ""):gsub("}", "") + + -- split the value_str, e.g. 'foo.bar.baz' -> {'foo', 'bar', 'baz'} + local splitted_values = {} + for var in match:gmatch("([^.]+)") do + -- try to parse 'var' as number + local x = tonumber(var) + if x then + var = x + 1 + end + table.insert(splitted_values, var) + end + + local result = vim.api.nvim_get_var("req_var_store") + if not result.__loaded then + error( + string.format( + "rest-nvim's global JSON variable has been unset, it is needed to get %s from there", + match + ) + ) + end + for _, val in pairs(splitted_values) do + if result[val] then + result = result[val] + else + result = "" + break + end + end + + return result +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 diff --git a/plugin/rest-nvim.vim b/plugin/rest-nvim.vim index 110b2d1f..21a2c32c 100644 --- a/plugin/rest-nvim.vim +++ b/plugin/rest-nvim.vim @@ -12,6 +12,12 @@ nnoremap RestNvimLast :lua require('rest-nvim').last() command! -nargs=? -complete=file RestSelectEnv :lua require('rest-nvim').select_env() +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 diff --git a/tests/request_vars/basic_usage.http b/tests/request_vars/basic_usage.http new file mode 100644 index 00000000..05a50d55 --- /dev/null +++ b/tests/request_vars/basic_usage.http @@ -0,0 +1,17 @@ +page: {{response.page}} +url: {{response.support.url}} +userid: {{response.data.0.id}} + +# normal var +foo: 4 + +#@response +GET https://reqres.in/api/users?page=2 + +GET https://reqres.in/api/users?page={{foo}} + +GET https://reqres.in/api/users?page={{page}} + +GET {{url}} + +GET https://reqres.in/api/users/{{userid}}