diff --git a/lua/hurl/history.lua b/lua/hurl/history.lua index 4da3205..079b33f 100644 --- a/lua/hurl/history.lua +++ b/lua/hurl/history.lua @@ -1,38 +1,56 @@ local utils = require('hurl.utils') local M = {} ---- Show the last request in the history ----@param response table -M.show = function(response) - local container = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) - if not response.headers then - -- Do not show anything if there is no response +-- Store the last 10 responses +local response_history = {} +local max_history_size = 10 + +-- Add a response to the history +local function add_to_history(response) + table.insert(response_history, 1, response) + if #response_history > max_history_size then + table.remove(response_history) + end +end + +-- Show the last response +function M.show_last_response() + if #response_history == 0 then + utils.notify('No response history available', vim.log.levels.INFO) return end - local content_type = response.headers['content-type'] - or response.headers['Content-Type'] - or response.headers['Content-type'] - or 'unknown' + local last_response = response_history[1] + local display = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) - utils.log_info('Detected content type: ' .. content_type) - if response.headers['content-length'] == '0' then - utils.log_info('hurl: empty response') - utils.notify('hurl: empty response', vim.log.levels.INFO) - end - if utils.is_json_response(content_type) then - container.show(response, 'json') + display.show(last_response, last_response.display_type or 'text') +end + +-- Function to be called after each successful request +function M.update_history(response) + -- Ensure response_time is a number + response.response_time = tonumber(response.response_time) or '-' + + -- Determine the content type and set display_type + local content_type = response.headers['Content-Type'] + or response.headers['content-type'] + or 'text/plain' + + if content_type:find('json') then + response.display_type = 'json' + elseif content_type:find('html') then + response.display_type = 'html' + elseif content_type:find('xml') then + response.display_type = 'xml' else - if utils.is_html_response(content_type) then - container.show(response, 'html') - else - if utils.is_xml_response(content_type) then - container.show(response, 'xml') - else - container.show(response, 'text') - end - end + response.display_type = 'text' end + + add_to_history(response) +end + +function M.get_last_response() + return response_history[1] end return M diff --git a/lua/hurl/lib/hurl_runner.lua b/lua/hurl/lib/hurl_runner.lua index b3580f6..4ac7dd0 100644 --- a/lua/hurl/lib/hurl_runner.lua +++ b/lua/hurl/lib/hurl_runner.lua @@ -1,28 +1,31 @@ local hurl_parser = require('hurl.lib.hurl_parser') local utils = require('hurl.utils') local spinner = require('hurl.spinner') +local history = require('hurl.history') local M = {} +M.is_running = false +M.start_time = nil +M.response = {} +M.head_state = '' ---- Pretty print body ----@param body string ----@param content_type string ----@return string[] -local function pretty_print_body(body, content_type) - local formatters = _HURL_GLOBAL_CONFIG.formatters - - if content_type:find('json') then - utils.log_info('Pretty print JSON body') - return utils.format(body, 'json') or {} - elseif content_type:find('html') then - utils.log_info('Pretty print HTML body') - return utils.format(body, 'html') or {} - elseif content_type:find('xml') then - utils.log_info('Pretty print XML body') - return utils.format(body, 'xml') or {} - else - utils.log_info('Pretty print text body') - return vim.split(body, '\n') +--- Log the Hurl command +---@param cmd table +local function log_hurl_command(cmd) + local command_str = table.concat(cmd, ' ') + _HURL_GLOBAL_CONFIG.last_hurl_command = command_str + utils.log_info('hurl: running command: ' .. command_str) +end + +--- Save captures as global variables +---@param captures table +local function save_captures_as_globals(captures) + if _HURL_GLOBAL_CONFIG.save_captures_as_globals and captures then + for key, value in pairs(captures) do + _HURL_GLOBAL_CONFIG.global_vars = _HURL_GLOBAL_CONFIG.global_vars or {} + _HURL_GLOBAL_CONFIG.global_vars[key] = value + utils.log_info(string.format('hurl: saved capture %s = %s as global variable', key, value)) + end end end @@ -31,7 +34,8 @@ end ---@param fromEntry integer ---@param toEntry integer ---@param isVeryVerbose boolean -function M.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose) +---@param additionalArgs table +function M.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose, additionalArgs) local args = { filePath } table.insert(args, isVeryVerbose and '--very-verbose' or '--verbose') if fromEntry then @@ -43,6 +47,11 @@ function M.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose) table.insert(args, tostring(toEntry)) end + -- Add additional arguments (like --json) + if additionalArgs then + vim.list_extend(args, additionalArgs) + end + -- Inject environment variables from .env files local env_files = _HURL_GLOBAL_CONFIG.find_env_files_in_folders() for _, env in ipairs(env_files) do @@ -80,33 +89,19 @@ function M.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose) local stdout_data = '' local stderr_data = '' - -- Create a new split and buffer for output - vim.cmd('vsplit') - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_win_set_buf(0, buf) - vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown') - - -- Set a buffer-local keymap to close the buffer with 'q' - vim.api.nvim_buf_set_keymap( - buf, - 'n', - _HURL_GLOBAL_CONFIG.mappings.close, - ':bd', - { noremap = true, silent = true } - ) - - -- Function to append lines to the buffer - local function append_to_buffer(lines) - vim.api.nvim_buf_set_lines(buf, -1, -1, false, lines) - end + -- Log the Hurl command + local hurl_command = 'hurl ' .. table.concat(args, ' ') + log_hurl_command({ 'hurl', unpack(args) }) - -- Show the command being run in the buffer - local command_str = 'hurl ' .. table.concat(args, ' ') - append_to_buffer({ '```sh', command_str, '```' }) + -- Always use split mode for verbose commands + local display = require('hurl.split') - -- Show the spinner + -- Clear the display and show processing message + display.clear() spinner.show() + local start_time = vim.loop.hrtime() + -- Start the Hurl command asynchronously vim.fn.jobstart({ 'hurl', unpack(args) }, { stdout_buffered = true, @@ -114,35 +109,40 @@ function M.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose) on_stdout = function(_, data) if data then stdout_data = stdout_data .. table.concat(data, '\n') - append_to_buffer(data) end end, on_stderr = function(_, data) if data then stderr_data = stderr_data .. table.concat(data, '\n') - append_to_buffer(data) end end, on_exit = function(_, code) -- Hide the spinner spinner.hide() + local end_time = vim.loop.hrtime() + local response_time = (end_time - start_time) / 1e6 -- Convert to milliseconds + if code ~= 0 then utils.log_info('Hurl command failed with code ' .. code) - append_to_buffer({ '# Error', stderr_data }) + display.show({ body = '# Hurl Error\n\n```sh\n' .. stderr_data .. '\n```' }, 'markdown') return end utils.log_info('Hurl command executed successfully') - -- Reset the buffer - vim.api.nvim_buf_set_lines(buf, 0, -1, false, {}) - -- Parse the output using the hurl_parser local result = hurl_parser.parse_hurl_output(stderr_data, stdout_data) - -- Format and display the parsed result + -- Format the parsed result local output_lines = {} + table.insert(output_lines, '# Hurl Command') + table.insert(output_lines, '') + table.insert(output_lines, '```sh') + table.insert(output_lines, hurl_command) + table.insert(output_lines, '```') + table.insert(output_lines, '') + for index, entry in ipairs(result.entries) do -- Request table.insert(output_lines, '# Request #' .. index) @@ -158,65 +158,251 @@ function M.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose) table.insert(output_lines, '```') table.insert(output_lines, '') - -- Only show the body for the last entry - if index == #result.entries then - -- Determine the content type for formatting - local content_type = entry.response.headers['Content-Type'] - or entry.response.headers['content-type'] - or '' - utils.log_info('Content-Type: ' .. content_type) - local body_format = 'text' - if content_type:find('json') then - body_format = 'json' - elseif content_type:find('text/html') then - body_format = 'html' - elseif content_type:find('application/xml') or content_type:find('text/xml') then - body_format = 'xml' - end - - table.insert(output_lines, '### Body:') - table.insert(output_lines, '```' .. body_format) - local formatted_body = pretty_print_body(entry.response.body, content_type) - for _, line in ipairs(formatted_body) do - table.insert(output_lines, line) - end - table.insert(output_lines, '```') - end - - table.insert(output_lines, '') + -- Headers table.insert(output_lines, '### Headers:') for key, value in pairs(entry.response.headers) do table.insert(output_lines, '- **' .. key .. '**: ' .. value) end + table.insert(output_lines, '') - -- Only show captures if there are any - if entry.captures then + -- Body + table.insert(output_lines, '### Body:') + table.insert(output_lines, '```json') + local formatted_body = utils.format(entry.response.body, 'json') + for _, line in ipairs(formatted_body or {}) do + table.insert(output_lines, line) + end + table.insert(output_lines, '```') + + -- Captures + if entry.captures and next(entry.captures) then table.insert(output_lines, '') table.insert(output_lines, '### Captures:') for key, value in pairs(entry.captures) do table.insert(output_lines, '- **' .. key .. '**: ' .. value) - -- Save captures as global variables in _HURL_GLOBAL_CONFIG if the option is enabled - if _HURL_GLOBAL_CONFIG.save_captures_as_globals then - _HURL_GLOBAL_CONFIG.global_vars = _HURL_GLOBAL_CONFIG.global_vars or {} - _HURL_GLOBAL_CONFIG.global_vars[key] = value - end end end - -- Show timings if any + -- Timings + table.insert(output_lines, '') + table.insert(output_lines, '### Timing:') + table.insert( + output_lines, + string.format('- **Total Response Time**: %.2f ms', response_time) + ) if entry.timings then - table.insert(output_lines, '') - table.insert(output_lines, '### Timing:') for key, value in pairs(entry.timings) do - table.insert(output_lines, '- **' .. key .. '**: ' .. value) + table.insert(output_lines, string.format('- **%s**: %s', key, value)) end end table.insert(output_lines, '---') + + -- Update history for each entry + local display_data = { + headers = entry.response.headers, + body = entry.response.body, + response_time = response_time, + status = entry.response.status, + url = entry.requestUrl, + method = entry.requestMethod, + curl_command = entry.curlCommand, + hurl_command = hurl_command, + captures = entry.captures, + timings = entry.timings, + } + history.update_history(display_data) + + -- Save captures as global variables + save_captures_as_globals(entry.captures) end - -- Append the formatted output to the buffer - append_to_buffer(output_lines) + -- Show the result using the display module + display.show({ body = table.concat(output_lines, '\n') }, 'markdown') + end, + }) +end + +--- Execute Hurl command +---@param opts table The options +---@param callback? function The callback function +function M.execute_hurl_cmd(opts, callback) + -- Check if a request is currently running + if M.is_running then + utils.log_info('hurl: request is already running') + utils.notify('hurl: request is running. Please try again later.', vim.log.levels.INFO) + return + end + + M.is_running = true + M.start_time = vim.loop.hrtime() -- Capture the start time + spinner.show() + M.head_state = '' + utils.log_info('hurl: running request') + utils.notify('hurl: running request', vim.log.levels.INFO) + + local is_json_mode = vim.tbl_contains(opts, '--json') + local is_file_mode = utils.has_file_in_opts(opts) + + -- Add verbose mode by default if not in JSON mode + if not is_json_mode then + table.insert(opts, '--verbose') + end + + -- Check vars.env exist on the current file buffer + -- Then inject the command with --variables-file vars.env + local env_files = _HURL_GLOBAL_CONFIG.find_env_files_in_folders() + for _, env in ipairs(env_files) do + utils.log_info( + 'hurl: looking for ' .. vim.inspect(_HURL_GLOBAL_CONFIG.env_file) .. ' in ' .. env.path + ) + if vim.fn.filereadable(env.path) == 1 then + utils.log_info('hurl: found env file in ' .. env.path) + table.insert(opts, '--variables-file') + table.insert(opts, env.path) + end + end + + -- Inject global variables into the command + if _HURL_GLOBAL_CONFIG.global_vars then + for var_name, var_value in pairs(_HURL_GLOBAL_CONFIG.global_vars) do + table.insert(opts, '--variable') + table.insert(opts, var_name .. '=' .. var_value) + end + end + + -- Inject fixture variables into the command + if _HURL_GLOBAL_CONFIG.fixture_vars then + for _, fixture in pairs(_HURL_GLOBAL_CONFIG.fixture_vars) do + table.insert(opts, '--variable') + table.insert(opts, fixture.name .. '=' .. fixture.callback()) + end + end + + local cmd = vim.list_extend({ 'hurl' }, opts) + if is_file_mode then + local file_root = _HURL_GLOBAL_CONFIG.file_root or vim.fn.getcwd() + vim.list_extend(cmd, { '--file-root', file_root }) + end + M.response = {} + + log_hurl_command(cmd) + + -- Clear the display and show processing message with Hurl command + local display = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) + display.clear() + + local stdout_data = '' + local stderr_data = '' + + vim.fn.jobstart(cmd, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = function(_, data) + if data then + stdout_data = stdout_data .. table.concat(data, '\n') + end + end, + on_stderr = function(_, data) + if data then + stderr_data = stderr_data .. table.concat(data, '\n') + end + end, + on_exit = function(_, code) + M.is_running = false + spinner.hide() + + if code ~= 0 then + utils.log_error('Hurl command failed with code ' .. code) + utils.notify('Hurl command failed. Check the split view for details.', vim.log.levels.ERROR) + + -- Show error in split view + local display = require('hurl.split') + local error_data = { + body = '# Hurl Error\n\n```sh\n' .. stderr_data .. '\n```', + headers = {}, + method = 'ERROR', + url = 'N/A', + status = code, + response_time = 0, + curl_command = 'N/A', + } + display.show(error_data, 'markdown') + return + end + + utils.log_info('hurl: request finished') + utils.notify('hurl: request finished', vim.log.levels.INFO) + + -- Calculate the response time + local end_time = vim.loop.hrtime() + M.response.response_time = (end_time - M.start_time) / 1e6 -- Convert to milliseconds + + if is_json_mode then + M.response.body = stdout_data + M.response.display_type = 'json' + if callback then + return callback(M.response) + end + else + -- Parse the output using the hurl_parser + local result = hurl_parser.parse_hurl_output(stderr_data, stdout_data) + + -- Display the result using popup or split based on the configuration + local display = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) + + -- Prepare the data for display + local last_entry = result.entries[#result.entries] + local display_data = { + headers = last_entry.response.headers, + body = last_entry.response.body, + response_time = M.response.response_time, + status = last_entry.response.status, + url = last_entry.requestUrl, + method = last_entry.requestMethod, + curl_command = last_entry.curlCommand, + } + + -- Separate headers from body + local body_start = display_data.body:find('\n\n') + if body_start then + local headers_str = display_data.body:sub(1, body_start - 1) + display_data.body = display_data.body:sub(body_start + 2) + + -- Parse additional headers from the body + for header in headers_str:gmatch('([^\n]+)') do + local key, value = header:match('([^:]+):%s*(.*)') + if key and value then + display_data.headers[key] = value + end + end + end + + -- Determine the content type + local content_type = display_data.headers['Content-Type'] + or display_data.headers['content-type'] + or 'text/plain' + + local display_type = 'text' + if content_type:find('json') then + display_type = 'json' + elseif content_type:find('html') then + display_type = 'html' + elseif content_type:find('xml') then + display_type = 'xml' + end + + display_data.display_type = display_type + + display.show(display_data, display_type) + + history.update_history(display_data) + + -- Save captures as global variables + if result.entries and #result.entries > 0 then + save_captures_as_globals(result.entries[#result.entries].captures) + end + end end, }) end diff --git a/lua/hurl/main.lua b/lua/hurl/main.lua index 33790c7..648aee4 100644 --- a/lua/hurl/main.lua +++ b/lua/hurl/main.lua @@ -6,280 +6,16 @@ local codelens = require('hurl.codelens') local M = {} -local response = {} -local head_state = '' -local is_running = false -local start_time = nil - ---- Convert from --json flag to same format with other command -local function convert_headers(headers) - local converted_headers = {} - - for _, header in ipairs(headers) do - converted_headers[header.name] = header.value - end - - return converted_headers -end - --- NOTE: Check the output with below command --- hurl example/dogs.hurl --json | jq -local on_json_output = function(code, data, event) - utils.log_info('hurl: on_output ' .. vim.inspect(code) .. vim.inspect(data) .. vim.inspect(event)) - - -- Remove the first element if it is an empty string - if data[1] == '' then - table.remove(data, 1) - end - -- If there is no data, return early - if not data[1] then - return - end - - local result = vim.json.decode(data[1]) - utils.log_info('hurl: json result ' .. vim.inspect(result)) - response.response_time = result.time - - -- TODO: It might have more than 1 entry, so we need to handle it - if - result - and result.entries - and result.entries[1] - and result.entries[1].calls - and result.entries[1].calls[1] - and result.entries[1].calls[1].response - then - local converted_headers = convert_headers(result.entries[1].calls[1].response.headers) - -- Add the status code and time to the headers - converted_headers.status = result.entries[1].calls[1].response.status - - response.headers = converted_headers - response.status = result.entries[1].calls[1].response.status - end - - response.body = { - vim.json.encode({ - msg = 'The flag --json does not contain the body yet. Refer to https://github.com/Orange-OpenSource/hurl/issues/1907', - }), - } -end - ---- Output handler ----@class Output -local on_output = function(code, data, event) - utils.log_info('hurl: on_output ' .. vim.inspect(code) .. vim.inspect(data)) - - if data[1] == '' then - table.remove(data, 1) - end - if not data[1] then - return - end - - if event == 'stderr' and #data > 1 then - response.body = data - utils.log_error(vim.inspect(data)) - response.raw = data - response.headers = {} - return - end - - if head_state == 'body' then - -- Append the data to the body if we are in the body state - utils.log_info('hurl: append data to body' .. vim.inspect(data)) - response.body = response.body or '' - response.body = response.body .. table.concat(data, '\n') - return - end - - -- TODO: The header parser sometime not working properly, e.g: https://google.com - local status = tonumber(string.match(data[1], '([%w+]%d+)')) - head_state = 'start' - if status then - response.status = status - response.headers = { status = data[1] } - response.headers_str = data[1] .. '\r\n' - end - - for i = 2, #data do - local line = data[i] - if line == '' or line == nil then - head_state = 'body' - elseif head_state == 'start' then - local key, value = string.match(line, '([%w-]+):%s*(.+)') - if key and value then - response.headers[key] = value - response.headers_str = response.headers_str .. line .. '\r\n' - end - elseif head_state == 'body' then - response.body = response.body or '' - response.body = response.body .. line - end - end - response.raw = data - - utils.log_info('hurl: response status ' .. response.status) - utils.log_info('hurl: response headers ' .. vim.inspect(response.headers)) - if response.body then - utils.log_info('hurl: response body ' .. response.body) - else - -- Fall back to empty string for non-body responses - response.body = '' - end -end - ---- Call hurl command ----@param opts table The options ----@param callback? function The callback function -local function execute_hurl_cmd(opts, callback) - -- Check if a request is currently running - if is_running then - utils.log_info('hurl: request is already running') - utils.notify('hurl: request is running. Please try again later.', vim.log.levels.INFO) - return - end - - is_running = true - start_time = vim.loop.hrtime() -- Capture the start time - spinner.show() - head_state = '' - utils.log_info('hurl: running request') - utils.notify('hurl: running request', vim.log.levels.INFO) - - local is_verbose_mode = vim.tbl_contains(opts, '--verbose') - local is_json_mode = vim.tbl_contains(opts, '--json') - local is_file_mode = utils.has_file_in_opts(opts) - - if - not _HURL_GLOBAL_CONFIG.auto_close - and not is_verbose_mode - and not is_json_mode - and response.body - then - local container = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) - utils.log_info('hurl: clear previous response if this is not auto close') - container.clear() - end - - -- Check vars.env exist on the current file buffer - -- Then inject the command with --variables-file vars.env - local env_files = _HURL_GLOBAL_CONFIG.find_env_files_in_folders() - for _, env in ipairs(env_files) do - utils.log_info( - 'hurl: looking for ' .. vim.inspect(_HURL_GLOBAL_CONFIG.env_file) .. ' in ' .. env.path - ) - if vim.fn.filereadable(env.path) == 1 then - utils.log_info('hurl: found env file in ' .. env.path) - table.insert(opts, '--variables-file') - table.insert(opts, env.path) - end - end - - -- Inject global variables into the command - if _HURL_GLOBAL_CONFIG.global_vars then - for var_name, var_value in pairs(_HURL_GLOBAL_CONFIG.global_vars) do - table.insert(opts, '--variable') - table.insert(opts, var_name .. '=' .. var_value) - end - end - - -- Inject fixture variables into the command - -- This is a workaround to inject dynamic variables into the hurl command, refer https://github.com/Orange-OpenSource/hurl/issues?q=sort:updated-desc+is:open+label:%22topic:+generators%22 - if _HURL_GLOBAL_CONFIG.fixture_vars then - for _, fixture in pairs(_HURL_GLOBAL_CONFIG.fixture_vars) do - table.insert(opts, '--variable') - table.insert(opts, fixture.name .. '=' .. fixture.callback()) - end - end - - -- Include the HTTP headers in the output and do not colorize output. - local cmd = vim.list_extend({ 'hurl', '-i', '--no-color' }, opts) - if is_file_mode then - local file_root = _HURL_GLOBAL_CONFIG.file_root or vim.fn.getcwd() - vim.list_extend(cmd, { '--file-root', file_root }) - end - response = {} - - utils.log_info('hurl: running command' .. vim.inspect(cmd)) - - vim.fn.jobstart(cmd, { - on_stdout = callback or (is_json_mode and on_json_output or on_output), - on_stderr = callback or (is_json_mode and on_json_output or on_output), - on_exit = function(i, code) - utils.log_info('exit at ' .. i .. ' , code ' .. code) - is_running = false - spinner.hide() - if code ~= 0 then - -- Send error code and response to quickfix and open it - -- It should display the error message - vim.fn.setqflist({}, 'r', { - title = 'hurl', - lines = response.raw or response.body, - }) - vim.fn.setqflist({}, 'a', { - title = 'hurl', - lines = { response.headers_str }, - }) - vim.cmd('copen') - return - end - - utils.log_info('hurl: request finished') - utils.notify('hurl: request finished', vim.log.levels.INFO) - - -- Calculate the response time - local end_time = vim.loop.hrtime() - response.response_time = (end_time - start_time) / 1e6 -- Convert to milliseconds - - if callback then - return callback(response) - else - -- show messages - local lines = response.raw or response.body - if #lines == 0 then - return - end - - local content_type = response.headers['content-type'] - or response.headers['Content-Type'] - or response.headers['Content-type'] - or 'unknown' - - utils.log_info('Detected content type: ' .. content_type) - if response.headers['content-length'] == '0' then - utils.log_info('hurl: empty response') - utils.notify('hurl: empty response', vim.log.levels.INFO) - end - - local container = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) - if utils.is_json_response(content_type) then - container.show(response, 'json') - else - if utils.is_html_response(content_type) then - container.show(response, 'html') - else - if utils.is_xml_response(content_type) then - container.show(response, 'xml') - else - container.show(response, 'text') - end - end - end - end - end, - }) -end - --- Run current file --- It will throw an error if that is not valid hurl file ---@param opts table The options local function run_current_file(opts) opts = opts or {} table.insert(opts, vim.fn.expand('%:p')) - execute_hurl_cmd(opts) + hurl_runner.execute_hurl_cmd(opts) end ---- Create a temporary file with the lines to run +-- Create a temporary file with the lines to run ---@param lines string[] ---@param opts table The options ---@param callback? function The callback function @@ -294,7 +30,7 @@ local function run_lines(lines, opts, callback) -- Add the temporary file to the arguments table.insert(opts, fname) - execute_hurl_cmd(opts, callback) + hurl_runner.execute_hurl_cmd(opts, callback) -- Clean up the temporary file after a delay local timeout = 1000 @@ -309,7 +45,7 @@ local function run_lines(lines, opts, callback) end, timeout) end ---- Run selection +-- Run selection ---@param opts table The options local function run_selection(opts) opts = opts or {} @@ -321,7 +57,7 @@ local function run_selection(opts) run_lines(lines, opts) end ---- Run at current line +-- Run at current line ---@param start_line number ---@param end_line number ---@param opts table @@ -339,14 +75,14 @@ local function run_at_lines(start_line, end_line, opts, callback) run_lines(lines, opts, callback) end +-- Helper function to run verbose commands in split mode +local function run_verbose_command(filePath, fromEntry, toEntry, isVeryVerbose, additionalArgs) + hurl_runner.run_hurl_verbose(filePath, fromEntry, toEntry, isVeryVerbose, additionalArgs) +end + function M.setup() - local bufnr = vim.api.nvim_get_current_buf() - local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') - -- Only setup codelens if the filetype is 'hurl' - if filetype == 'hurl' then - -- Add virtual text for Hurl entries - codelens.add_virtual_text_for_hurl_entries() - end + -- Show virtual text for Hurl entries + codelens.add_virtual_text_for_hurl_entries() -- Run request for a range of lines or the entire file utils.create_cmd('HurlRunner', function(opts) @@ -400,7 +136,7 @@ function M.setup() end end, { nargs = '*', range = true }) - -- Add new command to change env file with input + -- Set the env file utils.create_cmd('HurlSetEnvFile', function(opts) local env_file = opts.fargs[1] if not env_file then @@ -435,7 +171,7 @@ function M.setup() end end - hurl_runner.run_hurl_verbose(filePath, fromEntry, toEntry, false) + run_verbose_command(filePath, fromEntry, toEntry, false) end, { nargs = '*', range = true }) -- Run Hurl in very verbose mode @@ -459,45 +195,31 @@ function M.setup() end end - hurl_runner.run_hurl_verbose(filePath, fromEntry, toEntry, true) + run_verbose_command(filePath, fromEntry, toEntry, true) end, { nargs = '*', range = true }) - -- NOTE: Get output from --json output - -- Run Hurl in JSON mode and send output to quickfix + -- Run Hurl in JSON mode utils.create_cmd('HurlJson', function(opts) - local is_support_hurl = utils.is_nightly() or utils.is_hurl_parser_available - local result = is_support_hurl and http.find_hurl_entry_positions_in_buffer() - or http.find_http_verb_positions_in_buffer() - if result.current > 0 and result.start_line and result.end_line then - utils.log_info( - 'hurl: running request at line ' .. result.start_line .. ' to ' .. result.end_line - ) - opts.fargs = opts.fargs or {} - opts.fargs = vim.list_extend(opts.fargs, { '--json' }) - - -- Clear quickfix list - vim.fn.setqflist({}, 'r', { - title = 'hurl', - lines = {}, - }) - run_at_lines(1, result.end_line, opts.fargs, function(code, data, event) - utils.log_info('hurl: verbose callback ' .. vim.inspect(code) .. vim.inspect(data)) - -- Only send to output if the data is json format - if #data > 1 and data ~= nil then - vim.fn.setqflist({}, 'a', { - title = 'hurl - data', - lines = data, - }) - end - - vim.cmd('copen') - end) - else - if result then - utils.log_info('hurl: not HTTP method found in the current line' .. result.start_line) + local filePath = vim.fn.expand('%:p') + local fromEntry = opts.fargs[1] and tonumber(opts.fargs[1]) or nil + local toEntry = opts.fargs[2] and tonumber(opts.fargs[2]) or nil + + -- Detect the current entry if fromEntry and toEntry are not provided + if not fromEntry or not toEntry then + local is_support_hurl = utils.is_nightly() or utils.is_hurl_parser_available + local result = is_support_hurl and http.find_hurl_entry_positions_in_buffer() + or http.find_http_verb_positions_in_buffer() + if result.current > 0 then + fromEntry = result.current + toEntry = result.current + else + utils.log_info('hurl: no HTTP method found in the current line') utils.notify('hurl: no HTTP method found in the current line', vim.log.levels.INFO) + return end end + + run_verbose_command(filePath, fromEntry, toEntry, false, { '--json' }) end, { nargs = '*', range = true }) utils.create_cmd('HurlSetVariable', function(opts) @@ -595,7 +317,15 @@ function M.setup() -- Show last request response utils.create_cmd('HurlShowLastResponse', function() local history = require('hurl.history') - history.show(response) + local last_response = history.get_last_response() + if last_response then + -- Ensure response_time is a number + last_response.response_time = tonumber(last_response.response_time) or '-' + local display = require('hurl.' .. _HURL_GLOBAL_CONFIG.mode) + display.show(last_response, 'json') + else + utils.notify('No response history available', vim.log.levels.INFO) + end end, { nargs = '*', range = true, diff --git a/lua/hurl/popup.lua b/lua/hurl/popup.lua index 82f2e2a..8d19a3b 100644 --- a/lua/hurl/popup.lua +++ b/lua/hurl/popup.lua @@ -3,6 +3,7 @@ local event = require('nui.utils.autocmd').event local Layout = require('nui.layout') local utils = require('hurl.utils') +local spinner = require('hurl.spinner') local M = {} @@ -77,16 +78,32 @@ M.show = function(data, type) -- Info popup content local info_lines = {} + -- Add request information + table.insert(info_lines, '# Request') + table.insert(info_lines, '') + table.insert(info_lines, string.format('**Method**: %s', data.method)) + table.insert(info_lines, string.format('**URL**: %s', data.url)) + table.insert(info_lines, string.format('**Status**: %s', data.status)) + table.insert(info_lines, '') + + -- Add curl command + table.insert(info_lines, '# Curl Command') + table.insert(info_lines, '') + table.insert(info_lines, '```bash') + table.insert(info_lines, data.curl_command or 'N/A') + table.insert(info_lines, '```') + table.insert(info_lines, '') + -- Add headers table.insert(info_lines, '# Headers') table.insert(info_lines, '') for key, value in pairs(data.headers) do - table.insert(info_lines, '- **' .. key .. '**: ' .. value) + table.insert(info_lines, string.format('- **%s**: %s', key, value)) end -- Add response time table.insert(info_lines, '') - table.insert(info_lines, '**Response Time**: ' .. data.response_time .. ' ms') + table.insert(info_lines, string.format('**Response Time**: %.2f ms', data.response_time)) -- Set info content vim.api.nvim_buf_set_lines(popups.info.bufnr, 0, -1, false, info_lines) @@ -123,9 +140,12 @@ M.clear = function() if not layout.winid then return end - -- Clear the buffer and adding `Processing...` message + -- Clear the buffer and add `Processing...` message with spinner and Hurl command for _, popup in pairs(popups) do - vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { 'Processing...' }) + vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { + 'Processing... ' .. spinner.get_spinner(), + _HURL_GLOBAL_CONFIG.last_hurl_command or 'N/A', + }) end end diff --git a/lua/hurl/spinner.lua b/lua/hurl/spinner.lua index cb5a78f..034bc58 100644 --- a/lua/hurl/spinner.lua +++ b/lua/hurl/spinner.lua @@ -7,6 +7,9 @@ local M = {} -- User configuration section local config = { + -- Show notification when done. + -- Set to false to disable. + show_notification = true, -- Name of the plugin. plugin = 'hurl.nvim', -- Spinner frames. @@ -55,13 +58,15 @@ function M.show(position) 0, 100, vim.schedule_wrap(function() - vim.api.nvim_buf_set_lines( - spinner_buf, - 0, - -1, - false, - { config.spinner_frames[spinner_index] } - ) + if vim.api.nvim_buf_is_valid(spinner_buf) then + vim.api.nvim_buf_set_lines( + spinner_buf, + 0, + -1, + false, + { config.spinner_frames[spinner_index] } + ) + end spinner_index = spinner_index % #config.spinner_frames + 1 end) ) @@ -81,7 +86,7 @@ function M.hide(show_msg) vim.api.nvim_buf_delete(spinner_buf, { force = true }) end - if show_msg or _HURL_GLOBAL_CONFIG.show_notification then + if config.show_notification or show_msg then vim.notify('Done!', vim.log.levels.INFO, { title = config.plugin }) end end diff --git a/lua/hurl/split.lua b/lua/hurl/split.lua index fd6871a..9ca5378 100644 --- a/lua/hurl/split.lua +++ b/lua/hurl/split.lua @@ -8,6 +8,7 @@ local split = Split({ }) local utils = require('hurl.utils') +local spinner = require('hurl.spinner') local M = {} @@ -15,7 +16,7 @@ local M = {} ---@param data table --- - body string --- - headers table ----@param type 'json' | 'html' | 'xml' | 'text' +---@param type 'json' | 'html' | 'xml' | 'text' | 'markdown' M.show = function(data, type) local function quit() vim.cmd(_HURL_GLOBAL_CONFIG.mappings.close) @@ -24,9 +25,9 @@ M.show = function(data, type) -- mount/open the component split:mount() - -- Create a custom filetype so that we can use https://github.com/folke/edgy.nvim to manage the window - -- E.g: { title = "Hurl Nvim", ft = "hurl-nvim" }, - vim.bo[split.bufnr].filetype = 'hurl-nvim' + -- Set a custom filetype for window management, but use markdown for syntax highlighting + vim.api.nvim_buf_set_option(split.bufnr, 'filetype', 'markdown') + vim.api.nvim_buf_set_var(split.bufnr, 'hurl_nvim_buffer', true) if _HURL_GLOBAL_CONFIG.auto_close then -- unmount component when buffer is closed @@ -37,43 +38,72 @@ M.show = function(data, type) local output_lines = {} - -- Add headers - table.insert(output_lines, '# Headers') - table.insert(output_lines, '') - for key, value in pairs(data.headers) do - table.insert(output_lines, '- **' .. key .. '**: ' .. value) - end + if type == 'markdown' then + -- For markdown, we just use the body as-is + output_lines = vim.split(data.body, '\n') + else + -- Add request information + table.insert(output_lines, '# Request') + table.insert(output_lines, '') + table.insert(output_lines, string.format('**Method**: %s', data.method or 'N/A')) + table.insert(output_lines, string.format('**URL**: %s', data.url or 'N/A')) + table.insert(output_lines, string.format('**Status**: %s', data.status or 'N/A')) + table.insert(output_lines, '') + + -- Add curl command + table.insert(output_lines, '# Curl Command') + table.insert(output_lines, '') + table.insert(output_lines, '```bash') + table.insert(output_lines, data.curl_command or 'N/A') + table.insert(output_lines, '```') + table.insert(output_lines, '') + + -- Add headers + table.insert(output_lines, '# Headers') + table.insert(output_lines, '') + if data.headers then + for key, value in pairs(data.headers) do + table.insert(output_lines, string.format('- **%s**: %s', key, value)) + end + else + table.insert(output_lines, 'No headers available') + end - -- Add response time - table.insert(output_lines, '') - table.insert(output_lines, '**Response Time**: ' .. data.response_time .. ' ms') - table.insert(output_lines, '') - - -- Add body - table.insert(output_lines, '# Body') - table.insert(output_lines, '') - table.insert(output_lines, '```' .. type) - local content = utils.format(data.body, type) - if content then - for _, line in ipairs(content) do - table.insert(output_lines, line) + -- Add response time + table.insert(output_lines, '') + local response_time = tonumber(data.response_time) or 0 + table.insert(output_lines, string.format('**Response Time**: %.2f ms', response_time)) + table.insert(output_lines, '') + + -- Add body + table.insert(output_lines, '# Body') + table.insert(output_lines, '') + table.insert(output_lines, '```' .. type) + local content = utils.format(data.body, type) + if content then + for _, line in ipairs(content) do + table.insert(output_lines, line) + end + else + table.insert(output_lines, 'No content') end - else - table.insert(output_lines, 'No content') + table.insert(output_lines, '```') end - table.insert(output_lines, '```') -- Set content vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, output_lines) - -- Set content to highlight, refer https://github.com/MunifTanjim/nui.nvim/issues/76#issuecomment-1001358770 - -- After 200ms, the highlight will be applied + -- Set content to highlight and configure folding vim.defer_fn(function() - -- Set filetype to markdown + -- Disable TreeSitter-based folding for this buffer + vim.api.nvim_buf_set_option(split.bufnr, 'foldmethod', 'manual') + + -- Set filetype to markdown for syntax highlighting vim.api.nvim_buf_set_option(split.bufnr, 'filetype', 'markdown') - -- recomputing foldlevel, this is needed if we setup foldexpr - vim.api.nvim_feedkeys('zx', 'n', true) - end, 200) + + -- Refresh folding + vim.cmd('normal! zx') + end, 0) split:map('n', _HURL_GLOBAL_CONFIG.mappings.close, function() quit() @@ -86,8 +116,16 @@ M.clear = function() return end - -- Clear the buffer and adding `Processing...` message - vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, { 'Processing...' }) + -- Clear the buffer and add `Processing...` message with the current Hurl command + vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, { + 'Processing...', + '', + '# Hurl Command', + '', + '```sh', + _HURL_GLOBAL_CONFIG.last_hurl_command or 'N/A', + '```', + }) end return M