all repos

curl.nvim @ a695cb7

use curl within neovim
2 files changed, 239 insertions(+), 412 deletions(-)
updaties
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-04-19 22:44:58 +0300
Authored at: 2026-04-19 13:59:57 +0300
Change ID: lrsxvsrtppqnytzqzqmryxoxzytwtnzl
Parent: 7c9dc5c
M lua/curl.lua
···
        1
        
        -local curl = {}

      
        2
        
        -curl.config = {

      
        3
        
        -  curl = nil, -- path to binary

      
        4
        
        -  default_flags = { "-i" },

      
        5
        
        -  open_cmd = "vsplit",

      
        6
        
        -  map_execute = "<CR>", -- in query buffer

      
        7
        
        -}

      
        8
        
        -

      
        9
        1
         local H = {

      
        10
        2
           cache_dir = vim.fs.joinpath(vim.fn.stdpath "data", "curl_cache"),

      
        11
        
        -  query_buf = nil,

      
        12
        
        -  query_file = nil,

      
        13
        3
           query_buffers = {},

      
        14
        4
           query_autocmd_set = false,

      
        15
        5
           output_buf = nil,

      ···
        17
        7
           running_request = nil,

      
        18
        8
         }

      
        19
        9
         

      
        20
        
        -local function validate_config(config)

      
        21
        
        -  vim.validate("curl.config", config, "table")

      
        22
        
        -  vim.validate("curl.config.curl", config.curl, { "string", "nil" })

      
        23
        
        -  vim.validate("curl.config.default_flags", config.default_flags, "table")

      
        24
        
        -  vim.validate("curl.config.open_cmd", config.open_cmd, "string")

      
        25
        
        -  vim.validate("curl.config.map_execute", config.map_execute, "string")

      
        26
        
        -  for i, flag in ipairs(config.default_flags) do

      
        27
        
        -    vim.validate("curl.config.default_flags[" .. i .. "]", flag, "string")

      
        
        10
        +local curl = {}

      
        
        11
        +curl.config = {

      
        
        12
        +  curl = nil, -- path to binary

      
        
        13
        +  default_flags = { "-i" },

      
        
        14
        +  open_cmd = "vsplit",

      
        
        15
        +  map_execute = "<CR>",

      
        
        16
        +}

      
        
        17
        +

      
        
        18
        +function curl.setup(opts)

      
        
        19
        +  opts = opts or {}

      
        
        20
        +  vim.validate("opts", opts, "table")

      
        
        21
        +  curl.config = vim.tbl_deep_extend("force", curl.config, opts)

      
        
        22
        +  vim.filetype.add { extension = { curl = "curl" } }

      
        
        23
        +

      
        
        24
        +  if not H.command_created then

      
        
        25
        +    vim.api.nvim_create_user_command("Curl", function()

      
        
        26
        +      curl.open()

      
        
        27
        +    end, { desc = "Open curl.nvim query buffer" })

      
        
        28
        +    H.command_created = true

      
        28
        29
           end

      
        29
        
        -end

      
        30
        30
         

      
        31
        
        -local function is_valid_buf(buf)

      
        32
        
        -  return type(buf) == "number" and buf > 0 and vim.api.nvim_buf_is_valid(buf)

      
        
        31
        +  if not H.query_autocmd_set then

      
        
        32
        +    vim.api.nvim_create_autocmd(

      
        
        33
        +      { "BufReadPost", "BufNewFile", "BufEnter", "BufWinEnter", "BufFilePost" },

      
        
        34
        +      {

      
        
        35
        +        group = vim.api.nvim_create_augroup("curl_nvim_query_files", { clear = true }),

      
        
        36
        +        pattern = "*.curl",

      
        
        37
        +        callback = function(args)

      
        
        38
        +          H.attach_curl_buffer(args.buf)

      
        
        39
        +        end,

      
        
        40
        +      }

      
        
        41
        +    )

      
        
        42
        +    for _, buf in ipairs(vim.api.nvim_list_bufs()) do

      
        
        43
        +      H.attach_curl_buffer(buf)

      
        
        44
        +    end

      
        
        45
        +    H.query_autocmd_set = true

      
        
        46
        +  end

      
        33
        47
         end

      
        34
        48
         

      
        35
        
        -local function is_valid_win(win)

      
        36
        
        -  return type(win) == "number" and win > 0 and vim.api.nvim_win_is_valid(win)

      
        
        49
        +function curl.open()

      
        
        50
        +  local query_buf = H.get_query_buffer()

      
        
        51
        +  if not query_buf then return end

      
        
        52
        +  local query_win = H.find_window(query_buf)

      
        
        53
        +  if query_win and vim.api.nvim_win_is_valid(query_win) then

      
        
        54
        +    vim.api.nvim_set_current_win(query_win)

      
        
        55
        +  else

      
        
        56
        +    vim.api.nvim_set_current_buf(query_buf)

      
        
        57
        +  end

      
        37
        58
         end

      
        38
        59
         

      
        39
        
        -local function set_buffer_option(buf, name, value)

      
        40
        
        -  vim.api.nvim_set_option_value(name, value, { buf = buf })

      
        41
        
        -end

      
        
        60
        +function curl.execute()

      
        
        61
        +  local query_buf = vim.api.nvim_get_current_buf()

      
        
        62
        +  local query_file = vim.api.nvim_buf_get_name(query_buf)

      
        
        63
        +  if not H.is_curl_file(query_file) then

      
        
        64
        +    vim.notify("[curl.nvim] execute from a .curl request buffer", vim.log.levels.WARN)

      
        
        65
        +    return

      
        
        66
        +  end

      
        
        67
        +  H.query_buffers[query_file] = query_buf

      
        42
        68
         

      
        43
        
        -local function configure_scratch_buffer(buf)

      
        44
        
        -  set_buffer_option(buf, "buftype", "nofile")

      
        45
        
        -  set_buffer_option(buf, "bufhidden", "hide")

      
        46
        
        -  set_buffer_option(buf, "swapfile", false)

      
        47
        
        -end

      
        
        69
        +  local lines = vim.api.nvim_buf_get_lines(query_buf, 0, -1, false)

      
        
        70
        +  local start_line, end_line = H.find_query_block(lines, vim.api.nvim_win_get_cursor(0)[1])

      
        
        71
        +  if not start_line then

      
        
        72
        +    vim.notify("[curl.nvim] cursor is not on a query line", vim.log.levels.INFO)

      
        
        73
        +    return

      
        
        74
        +  end

      
        
        75
        +

      
        
        76
        +  local command, err = H.build_command(vim.list_slice(lines, start_line, end_line))

      
        
        77
        +  if not command then

      
        
        78
        +    vim.notify("[curl.nvim] " .. (err or "invalid query"), vim.log.levels.WARN)

      
        
        79
        +    return

      
        
        80
        +  end

      
        
        81
        +

      
        
        82
        +  H.save_query_buffer(query_buf, query_file)

      
        
        83
        +  H.open_output_window(query_buf)

      
        
        84
        +

      
        
        85
        +  if H.running_request then

      
        
        86
        +    pcall(function()

      
        
        87
        +      H.running_request:kill(15)

      
        
        88
        +    end)

      
        
        89
        +    H.running_request = nil

      
        
        90
        +  end

      
        48
        91
         

      
        49
        
        -local function configure_query_buffer(buf)

      
        50
        
        -  set_buffer_option(buf, "bufhidden", "hide")

      
        51
        
        -  set_buffer_option(buf, "swapfile", false)

      
        52
        
        -  set_buffer_option(buf, "modifiable", true)

      
        
        92
        +  H.running_request = vim.system(command, { text = true }, function(result)

      
        
        93
        +    H.running_request = nil

      
        
        94
        +    local output = result.stdout or ""

      
        
        95
        +    if result.stderr and result.stderr ~= "" then

      
        
        96
        +      output = output == "" and result.stderr or (output .. "\n\n" .. result.stderr)

      
        
        97
        +    end

      
        
        98
        +    if output == "" then output = "curl exited with code " .. result.code end

      
        
        99
        +    H.write_formatted_output(output)

      
        
        100
        +  end)

      
        53
        101
         end

      
        54
        102
         

      
        55
        
        -local function trim_lines(lines)

      
        56
        
        -  while #lines > 1 and lines[#lines] == "" do

      
        57
        
        -    table.remove(lines)

      
        58
        
        -  end

      
        59
        
        -  return lines

      
        
        103
        +function H.is_valid_buf(buf)

      
        
        104
        +  return type(buf) == "number" and buf > 0 and vim.api.nvim_buf_is_valid(buf)

      
        60
        105
         end

      
        61
        106
         

      
        62
        
        -local function trim(s)

      
        63
        
        -  local from = s:match "^%s*()"

      
        64
        
        -  return from > #s and "" or s:match(".*%S", from)

      
        
        107
        +function H.is_curl_file(path)

      
        
        108
        +  return type(path) == "string" and path ~= "" and path:sub(-5) == ".curl"

      
        65
        109
         end

      
        66
        110
         

      
        67
        
        -local function is_query_start(line)

      
        
        111
        +function H.is_query_start(line)

      
        68
        112
           if not line then return false end

      
        69
        113
           return line:match "^%s*curl%s" or line:match "^%s*curl$" or line:match "^%s*https?://"

      
        70
        114
         end

      
        71
        115
         

      
        72
        
        -local function is_curl_file(path)

      
        73
        
        -  return type(path) == "string" and path ~= "" and path:sub(-5) == ".curl"

      
        74
        
        -end

      
        75
        
        -

      
        76
        
        -local function get_or_create_buffer(name)

      
        77
        
        -  local existing = vim.fn.bufnr("^" .. name .. "$")

      
        78
        
        -  if existing ~= -1 and is_valid_buf(existing) then return existing end

      
        
        116
        +local function save_query_buffer(buf, file)

      
        
        117
        +  if not H.is_valid_buf(buf) then return end

      
        
        118
        +  file = file or vim.api.nvim_buf_get_name(buf)

      
        
        119
        +  if not H.is_curl_file(file) then return end

      
        79
        120
         

      
        80
        
        -  local buf = vim.api.nvim_create_buf(false, true)

      
        81
        
        -  vim.api.nvim_buf_set_name(buf, name)

      
        82
        
        -  configure_scratch_buffer(buf)

      
        83
        
        -  return buf

      
        
        121
        +  local ok = pcall(vim.fn.writefile, vim.api.nvim_buf_get_lines(buf, 0, -1, false), file, "b")

      
        
        122
        +  if ok then

      
        
        123
        +    pcall(vim.api.nvim_set_option_value, "modified", false, { buf = buf })

      
        
        124
        +  else

      
        
        125
        +    vim.notify("[curl.nvim] failed to save request file: " .. file, vim.log.levels.ERROR)

      
        
        126
        +  end

      
        84
        127
         end

      
        85
        128
         

      
        86
        
        -local function ensure_dir(path)

      
        87
        
        -  if vim.fn.isdirectory(path) == 1 then return true end

      
        88
        
        -  return vim.fn.mkdir(path, "p") == 1

      
        89
        
        -end

      
        
        129
        +function H.query_cache_file()

      
        
        130
        +  if vim.fn.isdirectory(H.cache_dir) ~= 1 and vim.fn.mkdir(H.cache_dir, "p") ~= 1 then

      
        
        131
        +    vim.notify("[curl.nvim] failed to create cache dir: " .. H.cache_dir, vim.log.levels.ERROR)

      
        
        132
        +    return nil

      
        
        133
        +  end

      
        90
        134
         

      
        91
        
        -local function get_workspace_id()

      
        92
        135
           local cwd = vim.fn.getcwd()

      
        93
        136
           local base = vim.fn.fnamemodify(cwd, ":t")

      
        94
        137
           if base == "" then base = "root" end

      
        95
        
        -  return base .. "_" .. vim.fn.sha256(cwd):sub(1, 8)

      
        
        138
        +  return vim.fs.joinpath(H.cache_dir, base .. "_" .. vim.fn.sha256(cwd):sub(1, 8) .. ".curl")

      
        
        139
        +end

      
        
        140
        +

      
        
        141
        +function H.attach_curl_buffer(buf)

      
        
        142
        +  if not H.is_valid_buf(buf) then return false end

      
        
        143
        +  local name = vim.api.nvim_buf_get_name(buf)

      
        
        144
        +  if not H.is_curl_file(name) then return false end

      
        
        145
        +

      
        
        146
        +  vim.api.nvim_set_option_value("filetype", "curl", { buf = buf })

      
        
        147
        +  vim.api.nvim_set_option_value("syntax", "sh", { buf = buf })

      
        
        148
        +  vim.api.nvim_set_option_value("commentstring", "# %s", { buf = buf })

      
        
        149
        +  pcall(vim.treesitter.language.register, "bash", "curl")

      
        
        150
        +  vim.keymap.set("n", curl.config.map_execute, curl.execute, {

      
        
        151
        +    buffer = buf,

      
        
        152
        +    noremap = true,

      
        
        153
        +    silent = true,

      
        
        154
        +    desc = "Execute curl query under cursor",

      
        
        155
        +  })

      
        
        156
        +  H.query_buffers[name] = buf

      
        
        157
        +  return true

      
        96
        158
         end

      
        97
        159
         

      
        98
        
        -local function get_cache_file()

      
        99
        
        -  if not ensure_dir(H.cache_dir) then

      
        100
        
        -    vim.notify("[curl.nvim] failed to create cache dir: " .. H.cache_dir, vim.log.levels.ERROR)

      
        101
        
        -    return nil

      
        102
        
        -  end

      
        
        160
        +function H.get_query_buffer()

      
        
        161
        +  local file = H.query_cache_file()

      
        
        162
        +  if not file then return nil end

      
        103
        163
         

      
        104
        
        -  local cwd = vim.fn.getcwd()

      
        105
        
        -  local hash = vim.fn.sha256(cwd)

      
        106
        
        -  local new_file = vim.fs.joinpath(H.cache_dir, get_workspace_id() .. ".curl")

      
        107
        
        -  local old_file = vim.fs.joinpath(H.cache_dir, hash)

      
        
        164
        +  local buf = H.query_buffers[file]

      
        
        165
        +  if not H.is_valid_buf(buf) then

      
        
        166
        +    buf = vim.fn.bufadd(file)

      
        
        167
        +    vim.fn.bufload(buf)

      
        
        168
        +    vim.api.nvim_set_option_value("bufhidden", "hide", { buf = buf })

      
        
        169
        +    vim.api.nvim_set_option_value("swapfile", false, { buf = buf })

      
        
        170
        +    vim.api.nvim_set_option_value("modifiable", true, { buf = buf })

      
        108
        171
         

      
        109
        
        -  if vim.uv.fs_stat(old_file) then

      
        110
        
        -    if not vim.uv.fs_stat(new_file) then

      
        111
        
        -      vim.fn.rename(old_file, new_file)

      
        112
        
        -    else

      
        113
        
        -      vim.fn.rename(old_file, old_file .. ".archive")

      
        114
        
        -    end

      
        
        172
        +    vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "BufLeave", "VimLeavePre" }, {

      
        
        173
        +      group = vim.api.nvim_create_augroup("curl_nvim_cache_" .. buf, { clear = true }),

      
        
        174
        +      buffer = buf,

      
        
        175
        +      callback = function()

      
        
        176
        +        save_query_buffer(buf, file)

      
        
        177
        +      end,

      
        
        178
        +    })

      
        115
        179
           end

      
        116
        180
         

      
        117
        
        -  return new_file

      
        
        181
        +  H.attach_curl_buffer(buf)

      
        
        182
        +  return buf

      
        118
        183
         end

      
        119
        184
         

      
        120
        
        -local function save_query_buffer(buf, file)

      
        121
        
        -  if not is_valid_buf(buf) then return end

      
        122
        
        -  file = file or vim.api.nvim_buf_get_name(buf)

      
        123
        
        -  if not is_curl_file(file) then return end

      
        
        185
        +local function get_output_buffer()

      
        
        186
        +  if H.is_valid_buf(H.output_buf) then return H.output_buf end

      
        124
        187
         

      
        125
        
        -  local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)

      
        126
        
        -  local ok = pcall(vim.fn.writefile, lines, file, "b")

      
        127
        
        -  if ok then

      
        128
        
        -    pcall(vim.api.nvim_set_option_value, "modified", false, { buf = buf })

      
        
        188
        +  local existing = vim.fn.bufnr "^Curl output$"

      
        
        189
        +  if existing ~= -1 and H.is_valid_buf(existing) then

      
        
        190
        +    H.output_buf = existing

      
        129
        191
           else

      
        130
        
        -    vim.notify("[curl.nvim] failed to save request file: " .. file, vim.log.levels.ERROR)

      
        
        192
        +    H.output_buf = vim.api.nvim_create_buf(false, true)

      
        
        193
        +    vim.api.nvim_buf_set_name(H.output_buf, "Curl output")

      
        
        194
        +    vim.api.nvim_set_option_value("buftype", "nofile", { buf = H.output_buf })

      
        
        195
        +    vim.api.nvim_set_option_value("bufhidden", "hide", { buf = H.output_buf })

      
        
        196
        +    vim.api.nvim_set_option_value("swapfile", false, { buf = H.output_buf })

      
        131
        197
           end

      
        
        198
        +

      
        
        199
        +  vim.api.nvim_set_option_value("filetype", "json", { buf = H.output_buf })

      
        
        200
        +  vim.api.nvim_set_option_value("modifiable", false, { buf = H.output_buf })

      
        
        201
        +  return H.output_buf

      
        132
        202
         end

      
        133
        203
         

      
        134
        
        -local function find_window_in_current_tab(bufnr)

      
        
        204
        +function H.find_window(buf)

      
        135
        205
           for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do

      
        136
        
        -    if vim.api.nvim_win_get_buf(win) == bufnr then return win end

      
        
        206
        +    if vim.api.nvim_win_get_buf(win) == buf then return win end

      
        137
        207
           end

      
        138
        
        -  return nil

      
        139
        208
         end

      
        140
        209
         

      
        141
        
        -local function is_json_start_line(line)

      
        142
        
        -  return trim(line):match "^[%[%{]" ~= nil

      
        143
        
        -end

      
        
        210
        +function H.open_output_window(query_buf)

      
        
        211
        +  local output_buf = get_output_buffer()

      
        
        212
        +  local output_win = H.find_window(output_buf)

      
        
        213
        +  if output_win and vim.api.nvim_win_is_valid(output_win) then return output_win end

      
        144
        214
         

      
        145
        
        -local function schedule_output_write(text, is_json)

      
        146
        
        -  vim.schedule(function()

      
        147
        
        -    H.write_output(text, is_json)

      
        148
        
        -  end)

      
        
        215
        +  local query_win = H.find_window(query_buf) or vim.api.nvim_get_current_win()

      
        
        216
        +  if not vim.api.nvim_win_is_valid(query_win) then return nil end

      
        
        217
        +

      
        
        218
        +  vim.api.nvim_set_current_win(query_win)

      
        
        219
        +  vim.cmd(curl.config.open_cmd)

      
        
        220
        +  vim.api.nvim_win_set_buf(0, output_buf)

      
        
        221
        +  vim.api.nvim_set_current_win(query_win)

      
        
        222
        +  return vim.api.nvim_get_current_win()

      
        149
        223
         end

      
        150
        224
         

      
        151
        
        -local function extract_headers_and_json(output)

      
        152
        
        -  local normalized = output:gsub("\r\n", "\n"):gsub("\r", "\n")

      
        153
        
        -  if is_json_start_line(normalized) then return {}, normalized end

      
        
        225
        +function H.find_query_block(lines, cursor_line)

      
        
        226
        +  if not lines[cursor_line] or vim.trim(lines[cursor_line]) == "" then return nil end

      
        154
        227
         

      
        155
        
        -  local lines = vim.split(normalized, "\n", { plain = true })

      
        156
        
        -  local json_index = nil

      
        157
        
        -  for i, line in ipairs(lines) do

      
        158
        
        -    if is_json_start_line(line) then

      
        159
        
        -      json_index = i

      
        
        228
        +  local start_line

      
        
        229
        +  for i = cursor_line, 1, -1 do

      
        
        230
        +    if H.is_query_start(lines[i]) then

      
        
        231
        +      start_line = i

      
        160
        232
               break

      
        161
        233
             end

      
        162
        
        -  end

      
        163
        
        -

      
        164
        
        -  if not json_index then return nil, nil end

      
        165
        
        -

      
        166
        
        -  local headers = {}

      
        167
        
        -  for i = 1, json_index - 1 do

      
        168
        
        -    table.insert(headers, trim(lines[i]))

      
        
        234
        +    if vim.trim(lines[i]) == "" then break end

      
        169
        235
           end

      
        
        236
        +  if not start_line then return nil end

      
        170
        237
         

      
        171
        
        -  local json_lines = {}

      
        172
        
        -  for i = json_index, #lines do

      
        173
        
        -    table.insert(json_lines, lines[i])

      
        
        238
        +  local end_line = #lines

      
        
        239
        +  for i = start_line + 1, #lines do

      
        
        240
        +    if H.is_query_start(lines[i]) or vim.trim(lines[i]) == "" then

      
        
        241
        +      end_line = i - 1

      
        
        242
        +      break

      
        
        243
        +    end

      
        174
        244
           end

      
        175
        245
         

      
        176
        
        -  return headers, table.concat(json_lines, "\n")

      
        177
        
        -end

      
        178
        
        -

      
        179
        
        -local function merge_headers_and_json(headers, json)

      
        180
        
        -  if not headers or #headers == 0 then return json end

      
        181
        
        -  local merged = vim.deepcopy(headers)

      
        182
        
        -  vim.list_extend(merged, vim.split(json, "\n", { plain = true }))

      
        183
        
        -  return table.concat(merged, "\n")

      
        
        246
        +  if cursor_line < start_line or cursor_line > end_line then return nil end

      
        
        247
        +  return start_line, end_line

      
        184
        248
         end

      
        185
        249
         

      
        186
        
        -local function shell_split(input)

      
        187
        
        -  local args = {}

      
        188
        
        -  local current = {}

      
        189
        
        -  local quote = nil

      
        
        250
        +function H.shell_split(input)

      
        
        251
        +  local args, current, quote = {}, {}, nil

      
        190
        252
           local i = 1

      
        191
        
        -

      
        192
        253
           while i <= #input do

      
        193
        254
             local ch = input:sub(i, i)

      
        194
        
        -

      
        195
        255
             if quote == "'" then

      
        196
        256
               if ch == "'" then

      
        197
        257
                 quote = nil

      ···
        222
        282
                 table.insert(current, ch)

      
        223
        283
               end

      
        224
        284
             end

      
        225
        
        -

      
        226
        285
             i = i + 1

      
        227
        286
           end

      
        228
        287
         

      ···
        231
        290
           return args

      
        232
        291
         end

      
        233
        292
         

      
        234
        
        -local function resolve_query_buffer()

      
        235
        
        -  if is_valid_buf(H.query_buf) then return H.query_buf end

      
        236
        
        -

      
        237
        
        -  local current = vim.api.nvim_get_current_buf()

      
        238
        
        -  local current_name = vim.api.nvim_buf_get_name(current)

      
        239
        
        -  if is_curl_file(current_name) then

      
        240
        
        -    H.set_query_context(current, current_name)

      
        241
        
        -    return current

      
        242
        
        -  end

      
        243
        
        -

      
        244
        
        -  return H.get_query_buffer()

      
        245
        
        -end

      
        246
        
        -

      
        247
        
        -local function stop_running_request()

      
        248
        
        -  if not H.running_request then return end

      
        249
        
        -  pcall(function()

      
        250
        
        -    H.running_request:kill(15)

      
        251
        
        -  end)

      
        252
        
        -  H.running_request = nil

      
        253
        
        -end

      
        254
        
        -

      
        255
        
        -local function result_to_output(result)

      
        256
        
        -  local chunks = {}

      
        257
        
        -  if result.stdout and result.stdout ~= "" then table.insert(chunks, result.stdout) end

      
        258
        
        -  if result.stderr and result.stderr ~= "" then table.insert(chunks, result.stderr) end

      
        259
        
        -

      
        260
        
        -  if #chunks == 0 then return "curl exited with code " .. result.code end

      
        261
        
        -

      
        262
        
        -  return table.concat(chunks, "\n\n")

      
        263
        
        -end

      
        264
        
        -

      
        265
        
        -function H.set_query_mapping(buf)

      
        266
        
        -  if not is_valid_buf(buf) then return end

      
        267
        
        -

      
        268
        
        -  vim.keymap.set("n", curl.config.map_execute, curl.execute, {

      
        269
        
        -    buffer = buf,

      
        270
        
        -    noremap = true,

      
        271
        
        -    silent = true,

      
        272
        
        -    desc = "Execute curl query under cursor",

      
        273
        
        -  })

      
        274
        
        -end

      
        275
        
        -

      
        276
        
        -function H.attach_curl_buffer(buf)

      
        277
        
        -  if not is_valid_buf(buf) then return false end

      
        278
        
        -  local name = vim.api.nvim_buf_get_name(buf)

      
        279
        
        -  if not is_curl_file(name) then return false end

      
        280
        
        -

      
        281
        
        -  set_buffer_option(buf, "filetype", "curl")

      
        282
        
        -  set_buffer_option(buf, "syntax", "sh")

      
        283
        
        -  set_buffer_option(buf, "commentstring", "# %s")

      
        284
        
        -  pcall(vim.treesitter.language.register, "bash", "curl")

      
        285
        
        -  H.set_query_mapping(buf)

      
        286
        
        -  H.query_buffers[name] = buf

      
        287
        
        -  return true

      
        288
        
        -end

      
        289
        
        -

      
        290
        
        -local function create_query_buffer(file)

      
        291
        
        -  local buf = vim.fn.bufadd(file)

      
        292
        
        -  vim.fn.bufload(buf)

      
        293
        
        -  configure_query_buffer(buf)

      
        294
        
        -  H.attach_curl_buffer(buf)

      
        295
        
        -

      
        296
        
        -  local group = vim.api.nvim_create_augroup("curl_nvim_cache_" .. buf, { clear = true })

      
        297
        
        -  vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "BufLeave", "VimLeavePre" }, {

      
        298
        
        -    group = group,

      
        299
        
        -    buffer = buf,

      
        300
        
        -    callback = function()

      
        301
        
        -      save_query_buffer(buf, file)

      
        302
        
        -    end,

      
        303
        
        -  })

      
        304
        
        -

      
        305
        
        -  return buf

      
        306
        
        -end

      
        307
        
        -

      
        308
        
        -function H.set_query_context(buf, file)

      
        309
        
        -  if not is_valid_buf(buf) then return false end

      
        310
        
        -  file = file or vim.api.nvim_buf_get_name(buf)

      
        311
        
        -  if not is_curl_file(file) then return false end

      
        312
        
        -

      
        313
        
        -  H.query_buf = buf

      
        314
        
        -  H.query_file = file

      
        315
        
        -  H.query_buffers[file] = buf

      
        316
        
        -  return true

      
        317
        
        -end

      
        318
        
        -

      
        319
        
        -function H.setup_filetype()

      
        320
        
        -  vim.filetype.add {

      
        321
        
        -    extension = { curl = "curl" },

      
        322
        
        -  }

      
        323
        
        -end

      
        324
        
        -

      
        325
        
        -function H.setup_command()

      
        326
        
        -  if H.command_created then return end

      
        327
        
        -

      
        328
        
        -  vim.api.nvim_create_user_command("Curl", function()

      
        329
        
        -    curl.open()

      
        330
        
        -  end, { desc = "Open curl.nvim query buffer" })

      
        331
        
        -  H.command_created = true

      
        332
        
        -end

      
        333
        
        -

      
        334
        
        -function H.get_query_buffer()

      
        335
        
        -  local file = get_cache_file()

      
        336
        
        -  if not file then return nil end

      
        337
        
        -

      
        338
        
        -  local existing = H.query_buffers[file]

      
        339
        
        -  if not is_valid_buf(existing) then existing = create_query_buffer(file) end

      
        340
        
        -

      
        341
        
        -  H.query_buffers[file] = existing

      
        342
        
        -  H.set_query_context(existing, file)

      
        343
        
        -  H.set_query_mapping(existing)

      
        344
        
        -  return existing

      
        345
        
        -end

      
        346
        
        -

      
        347
        
        -function H.setup_query_file_autocmd()

      
        348
        
        -  if H.query_autocmd_set then return end

      
        349
        
        -

      
        350
        
        -  local group = vim.api.nvim_create_augroup("curl_nvim_query_files", { clear = true })

      
        351
        
        -  vim.api.nvim_create_autocmd(

      
        352
        
        -    { "BufReadPost", "BufNewFile", "BufEnter", "BufWinEnter", "BufFilePost" },

      
        353
        
        -    {

      
        354
        
        -      group = group,

      
        355
        
        -      pattern = "*.curl",

      
        356
        
        -      callback = function(args)

      
        357
        
        -        H.attach_curl_buffer(args.buf)

      
        358
        
        -      end,

      
        359
        
        -    }

      
        360
        
        -  )

      
        361
        
        -

      
        362
        
        -  for _, buf in ipairs(vim.api.nvim_list_bufs()) do

      
        363
        
        -    H.attach_curl_buffer(buf)

      
        364
        
        -  end

      
        365
        
        -

      
        366
        
        -  H.query_autocmd_set = true

      
        367
        
        -end

      
        368
        
        -

      
        369
        
        -function H.get_output_buffer()

      
        370
        
        -  if is_valid_buf(H.output_buf) then return H.output_buf end

      
        371
        
        -

      
        372
        
        -  H.output_buf = get_or_create_buffer "Curl output"

      
        373
        
        -  set_buffer_option(H.output_buf, "filetype", "json")

      
        374
        
        -  set_buffer_option(H.output_buf, "modifiable", false)

      
        375
        
        -  return H.output_buf

      
        376
        
        -end

      
        377
        
        -

      
        378
        
        -function H.open_output_window()

      
        379
        
        -  local output_buf = H.get_output_buffer()

      
        380
        
        -  local query_buf = resolve_query_buffer()

      
        381
        
        -  if not query_buf then return nil end

      
        382
        
        -

      
        383
        
        -  local query_win = find_window_in_current_tab(query_buf) or vim.api.nvim_get_current_win()

      
        384
        
        -  local output_win = find_window_in_current_tab(output_buf)

      
        385
        
        -

      
        386
        
        -  if is_valid_win(output_win) then return output_win end

      
        387
        
        -

      
        388
        
        -  vim.api.nvim_set_current_win(query_win)

      
        389
        
        -  vim.cmd(curl.config.open_cmd)

      
        390
        
        -  output_win = vim.api.nvim_get_current_win()

      
        391
        
        -  vim.api.nvim_win_set_buf(output_win, output_buf)

      
        392
        
        -  vim.api.nvim_set_current_win(query_win)

      
        393
        
        -  return output_win

      
        394
        
        -end

      
        395
        
        -

      
        396
        
        -function H.find_query_block(lines, cursor_line)

      
        397
        
        -  if not lines[cursor_line] or vim.trim(lines[cursor_line]) == "" then return nil end

      
        398
        
        -

      
        399
        
        -  local start_line = nil

      
        400
        
        -  for line_no = cursor_line, 1, -1 do

      
        401
        
        -    local line = lines[line_no]

      
        402
        
        -    if is_query_start(line) then

      
        403
        
        -      start_line = line_no

      
        404
        
        -      break

      
        405
        
        -    end

      
        406
        
        -    if vim.trim(line) == "" then break end

      
        407
        
        -  end

      
        408
        
        -  if not start_line then return nil end

      
        409
        
        -

      
        410
        
        -  local end_line = #lines

      
        411
        
        -  for line_no = start_line + 1, #lines do

      
        412
        
        -    local line = lines[line_no]

      
        413
        
        -    if is_query_start(line) or vim.trim(line) == "" then

      
        414
        
        -      end_line = line_no - 1

      
        415
        
        -      break

      
        416
        
        -    end

      
        417
        
        -  end

      
        418
        
        -

      
        419
        
        -  if cursor_line < start_line or cursor_line > end_line then return nil end

      
        420
        
        -  return start_line, end_line

      
        421
        
        -end

      
        422
        
        -

      
        423
        293
         function H.build_command(query_lines)

      
        424
        
        -  local binary = curl.config.curl or "curl"

      
        425
        294
           local body_parts = {}

      
        426
        
        -

      
        427
        295
           for _, line in ipairs(query_lines) do

      
        428
        
        -    local trimmed = vim.trim(line)

      
        429
        
        -    if trimmed ~= "" and not trimmed:match "^#" then

      
        430
        
        -      table.insert(body_parts, trimmed:gsub("\\%s*$", ""))

      
        431
        
        -    end

      
        
        296
        +    local s = vim.trim(line)

      
        
        297
        +    if s ~= "" and not s:match "^#" then table.insert(body_parts, s:gsub("\\%s*$", "")) end

      
        432
        298
           end

      
        433
        299
         

      
        434
        300
           local body = vim.trim(table.concat(body_parts, " "))

      
        435
        301
           if body == "" then return nil, "empty query" end

      
        436
        302
         

      
        437
        
        -  local parsed, parse_err = shell_split(body)

      
        438
        
        -  if not parsed or #parsed == 0 then return nil, parse_err end

      
        
        303
        +  local parsed, err = H.shell_split(body)

      
        
        304
        +  if not parsed or #parsed == 0 then return nil, err end

      
        439
        305
           if parsed[1] == "curl" then table.remove(parsed, 1) end

      
        440
        306
         

      
        441
        
        -  local command = { binary, "--silent", "--show-error" }

      
        442
        
        -  vim.list_extend(command, curl.config.default_flags)

      
        443
        
        -  vim.list_extend(command, parsed)

      
        444
        
        -  return command, nil

      
        445
        
        -end

      
        446
        
        -

      
        447
        
        -function H.write_formatted_output(output)

      
        448
        
        -  if output == "" or vim.fn.executable "jq" ~= 1 then

      
        449
        
        -    schedule_output_write(output, false)

      
        450
        
        -    return

      
        451
        
        -  end

      
        452
        
        -

      
        453
        
        -  local headers, json_candidate = extract_headers_and_json(output)

      
        454
        
        -  if not json_candidate or json_candidate == "" then

      
        455
        
        -    schedule_output_write(output, false)

      
        456
        
        -    return

      
        457
        
        -  end

      
        458
        
        -

      
        459
        
        -  vim.system({ "jq", "." }, {

      
        460
        
        -    text = true,

      
        461
        
        -    stdin = json_candidate,

      
        462
        
        -  }, function(result)

      
        463
        
        -    local text = output

      
        464
        
        -    local is_json = false

      
        465
        
        -

      
        466
        
        -    if result.code == 0 and result.stdout and result.stdout ~= "" then

      
        467
        
        -      is_json = true

      
        468
        
        -      text = merge_headers_and_json(headers, result.stdout)

      
        469
        
        -    end

      
        470
        
        -

      
        471
        
        -    schedule_output_write(text, is_json)

      
        472
        
        -  end)

      
        
        307
        +  local cmd = { curl.config.curl or "curl", "--silent", "--show-error" }

      
        
        308
        +  vim.list_extend(cmd, curl.config.default_flags or {})

      
        
        309
        +  vim.list_extend(cmd, parsed)

      
        
        310
        +  return cmd

      
        473
        311
         end

      
        474
        312
         

      
        475
        313
         function H.write_output(text, is_json)

      
        476
        
        -  local output_buf = H.get_output_buffer()

      
        477
        
        -  local lines = trim_lines(vim.split(text, "\n", { plain = true }))

      
        
        314
        +  local buf = get_output_buffer()

      
        
        315
        +  local lines = vim.split(text or "", "\n", { plain = true })

      
        
        316
        +  while #lines > 1 and lines[#lines] == "" do

      
        
        317
        +    table.remove(lines)

      
        
        318
        +  end

      
        478
        319
           if #lines == 0 then lines = { "" } end

      
        479
        320
         

      
        480
        
        -  set_buffer_option(output_buf, "modifiable", true)

      
        481
        
        -  vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, lines)

      
        482
        
        -  set_buffer_option(output_buf, "modifiable", false)

      
        483
        
        -  set_buffer_option(output_buf, "filetype", is_json and "json" or "text")

      
        
        321
        +  vim.api.nvim_set_option_value("modifiable", true, { buf = buf })

      
        
        322
        +  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)

      
        
        323
        +  vim.api.nvim_set_option_value("modifiable", false, { buf = buf })

      
        
        324
        +  vim.api.nvim_set_option_value("filetype", is_json and "json" or "text", { buf = buf })

      
        484
        325
         end

      
        485
        326
         

      
        486
        
        -function curl.setup(opts)

      
        487
        
        -  local config = vim.tbl_deep_extend("force", curl.config, opts or {})

      
        488
        
        -  validate_config(config)

      
        489
        
        -  curl.config = config

      
        490
        
        -

      
        491
        
        -  H.setup_filetype()

      
        492
        
        -  H.setup_command()

      
        493
        
        -  H.setup_query_file_autocmd()

      
        494
        
        -end

      
        495
        
        -

      
        496
        
        -function curl.open()

      
        497
        
        -  local query_buf = H.get_query_buffer()

      
        498
        
        -  if not query_buf then return end

      
        499
        
        -

      
        500
        
        -  local query_win = find_window_in_current_tab(query_buf)

      
        501
        
        -  if is_valid_win(query_win) then

      
        502
        
        -    vim.api.nvim_set_current_win(query_win)

      
        503
        
        -  else

      
        504
        
        -    vim.api.nvim_set_current_buf(query_buf)

      
        505
        
        -  end

      
        506
        
        -end

      
        507
        
        -

      
        508
        
        -function curl.execute()

      
        509
        
        -  local query_buf = vim.api.nvim_get_current_buf()

      
        510
        
        -  local query_file = vim.api.nvim_buf_get_name(query_buf)

      
        511
        
        -  if not is_curl_file(query_file) then

      
        512
        
        -    vim.notify("[curl.nvim] execute from a .curl request buffer", vim.log.levels.WARN)

      
        
        327
        +function H.write_formatted_output(output)

      
        
        328
        +  if output == "" or vim.fn.executable "jq" ~= 1 then

      
        
        329
        +    vim.schedule(function()

      
        
        330
        +      H.write_output(output, false)

      
        
        331
        +    end)

      
        513
        332
             return

      
        514
        333
           end

      
        515
        
        -  H.set_query_context(query_buf, query_file)

      
        516
        334
         

      
        517
        
        -  local lines = vim.api.nvim_buf_get_lines(query_buf, 0, -1, false)

      
        518
        
        -  local cursor_line = vim.api.nvim_win_get_cursor(0)[1]

      
        519
        
        -  local start_line, end_line = H.find_query_block(lines, cursor_line)

      
        520
        
        -  if not start_line then

      
        521
        
        -    vim.notify("[curl.nvim] cursor is not on a query line", vim.log.levels.INFO)

      
        522
        
        -    return

      
        
        335
        +  local lines = vim.split(output:gsub("\r\n", "\n"):gsub("\r", "\n"), "\n", { plain = true })

      
        
        336
        +  local json_index

      
        
        337
        +  for i, line in ipairs(lines) do

      
        
        338
        +    if vim.trim(line):match "^[%[%{]" then

      
        
        339
        +      json_index = i

      
        
        340
        +      break

      
        
        341
        +    end

      
        523
        342
           end

      
        524
        
        -

      
        525
        
        -  local query_lines = vim.list_slice(lines, start_line, end_line)

      
        526
        
        -  local command, command_err = H.build_command(query_lines)

      
        527
        
        -  if not command then

      
        528
        
        -    vim.notify("[curl.nvim] " .. (command_err or "invalid query"), vim.log.levels.WARN)

      
        
        343
        +  if not json_index then

      
        
        344
        +    vim.schedule(function()

      
        
        345
        +      H.write_output(output, false)

      
        
        346
        +    end)

      
        529
        347
             return

      
        530
        348
           end

      
        531
        349
         

      
        532
        
        -  save_query_buffer(query_buf, query_file)

      
        533
        
        -  H.open_output_window()

      
        534
        
        -  stop_running_request()

      
        
        350
        +  local headers = {}

      
        
        351
        +  for i = 1, json_index - 1 do

      
        
        352
        +    table.insert(headers, vim.trim(lines[i]))

      
        
        353
        +  end

      
        
        354
        +  local json_body = table.concat(vim.list_slice(lines, json_index, #lines), "\n")

      
        535
        355
         

      
        536
        
        -  H.running_request = vim.system(command, { text = true }, function(result)

      
        537
        
        -    H.running_request = nil

      
        538
        
        -    H.write_formatted_output(result_to_output(result))

      
        
        356
        +  vim.system({ "jq", "." }, { text = true, stdin = json_body }, function(result)

      
        
        357
        +    local text, is_json = output, false

      
        
        358
        +    if result.code == 0 and result.stdout and result.stdout ~= "" then

      
        
        359
        +      is_json = true

      
        
        360
        +      text = #headers > 0 and (table.concat(headers, "\n") .. "\n" .. result.stdout)

      
        
        361
        +        or result.stdout

      
        
        362
        +    end

      
        
        363
        +    vim.schedule(function()

      
        
        364
        +      H.write_output(text, is_json)

      
        
        365
        +    end)

      
        539
        366
           end)

      
        540
        367
         end

      
        541
        368
         

      
M lua/curl/health.lua
···
        1
        
        -local function check_binary(bin, msg, optional)

      
        
        1
        +local function check(bin, msg, optional)

      
        2
        2
           if vim.fn.executable(bin) == 1 then

      
        3
        3
             vim.health.ok(bin .. " found on PATH: `" .. vim.fn.exepath(bin) .. "`")

      
        4
        4
             return

      ···
        21
        21
             end

      
        22
        22
         

      
        23
        23
             vim.health.start "Required dependencies"

      
        24
        
        -    check_binary("curl", "required to run requests", false)

      
        
        24
        +    check("curl", "required to run requests", false)

      
        25
        25
         

      
        26
        26
             vim.health.start "Optional dependencies"

      
        27
        
        -    check_binary("jq", "used for JSON formatting in output buffers", true)

      
        
        27
        +    check("jq", "used for JSON formatting in output buffers", true)

      
        28
        28
           end,

      
        29
        29
         }