all repos

curl.nvim @ 3b9e8b5

use curl within neovim

curl.nvim/lua/curl.lua (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
bug fix; open result window only when we get response; formatting, 1 month ago
1
local H = {
2
  cache_dir = vim.fs.joinpath(vim.fn.stdpath "data", "curl_cache"),
3
  query_buffers = {},
4
  query_autocmd_set = false,
5
  output_buf = -1,
6
  command_created = false,
7
  running_request = nil,
8
}
9
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() curl.open() end, { desc = "Open curl.nvim query buffer" })
26
    H.command_created = true
27
  end
28
29
  if not H.query_autocmd_set then
30
    vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile", "BufEnter", "BufWinEnter", "BufFilePost" }, {
31
      group = vim.api.nvim_create_augroup("curl_nvim_query_files", { clear = true }),
32
      pattern = "*.curl",
33
      callback = function(args) H.attach_curl_buffer(args.buf) end,
34
    })
35
    for _, buf in ipairs(vim.api.nvim_list_bufs()) do
36
      H.attach_curl_buffer(buf)
37
    end
38
    H.query_autocmd_set = true
39
  end
40
end
41
42
function curl.open()
43
  local query_buf = H.get_query_buffer()
44
  if not query_buf then return end
45
  local query_win = H.find_window(query_buf)
46
  if query_win and vim.api.nvim_win_is_valid(query_win) then
47
    vim.api.nvim_set_current_win(query_win)
48
  else
49
    vim.api.nvim_set_current_buf(query_buf)
50
  end
51
end
52
53
function curl.execute()
54
  local query_buf = vim.api.nvim_get_current_buf()
55
  local query_file = vim.api.nvim_buf_get_name(query_buf)
56
  if not H.is_curl_file(query_file) then
57
    vim.notify("[curl.nvim] execute from a .curl request buffer", vim.log.levels.WARN)
58
    return
59
  end
60
  H.query_buffers[query_file] = query_buf
61
62
  local lines = vim.api.nvim_buf_get_lines(query_buf, 0, -1, false)
63
  local start_line, end_line = H.find_query_block(lines, vim.api.nvim_win_get_cursor(0)[1])
64
  if not start_line then
65
    vim.notify("[curl.nvim] cursor is not on a query line", vim.log.levels.INFO)
66
    return
67
  end
68
69
  local command, err = H.build_command(vim.list_slice(lines, start_line, end_line))
70
  if not command then
71
    vim.notify("[curl.nvim] " .. (err or "invalid query"), vim.log.levels.WARN)
72
    return
73
  end
74
75
  H.save_query_buffer(query_buf, query_file)
76
77
  if H.running_request then
78
    pcall(function() H.running_request:kill(15) end)
79
    H.running_request = nil
80
  end
81
82
  H.running_request = vim.system(command, { text = true }, function(result)
83
    H.running_request = nil
84
    local output = result.stdout or ""
85
    if result.stderr and result.stderr ~= "" then
86
      output = output == "" and result.stderr or (output .. "\n\n" .. result.stderr)
87
    end
88
    if output == "" then output = "curl exited with code " .. result.code end
89
    H.write_formatted_output(query_buf, output)
90
  end)
91
end
92
93
function H.is_valid_buf(buf) return type(buf) == "number" and buf > 0 and vim.api.nvim_buf_is_valid(buf) end
94
95
function H.is_curl_file(path) return type(path) == "string" and path ~= "" and path:sub(-5) == ".curl" end
96
97
function H.is_query_start(line)
98
  if not line then return false end
99
  return line:match "^%s*curl%s" or line:match "^%s*curl$" or line:match "^%s*https?://"
100
end
101
102
function H.save_query_buffer(buf, file)
103
  if not H.is_valid_buf(buf) then return end
104
  file = file or vim.api.nvim_buf_get_name(buf)
105
  if not H.is_curl_file(file) then return end
106
107
  local ok = pcall(vim.fn.writefile, vim.api.nvim_buf_get_lines(buf, 0, -1, false), file, "b")
108
  if ok then
109
    pcall(vim.api.nvim_set_option_value, "modified", false, { buf = buf })
110
  else
111
    vim.notify("[curl.nvim] failed to save request file: " .. file, vim.log.levels.ERROR)
112
  end
113
end
114
115
function H.query_cache_file()
116
  if vim.fn.isdirectory(H.cache_dir) ~= 1 and vim.fn.mkdir(H.cache_dir, "p") ~= 1 then
117
    vim.notify("[curl.nvim] failed to create cache dir: " .. H.cache_dir, vim.log.levels.ERROR)
118
    return nil
119
  end
120
121
  local cwd = vim.fn.getcwd()
122
  local base = vim.fn.fnamemodify(cwd, ":t")
123
  if base == "" then base = "root" end
124
  return vim.fs.joinpath(H.cache_dir, base .. "_" .. vim.fn.sha256(cwd):sub(1, 8) .. ".curl")
125
end
126
127
function H.attach_curl_buffer(buf)
128
  if not H.is_valid_buf(buf) then return false end
129
  local name = vim.api.nvim_buf_get_name(buf)
130
  if not H.is_curl_file(name) then return false end
131
132
  vim.api.nvim_set_option_value("filetype", "curl", { buf = buf })
133
  vim.api.nvim_set_option_value("syntax", "sh", { buf = buf })
134
  vim.api.nvim_set_option_value("commentstring", "# %s", { buf = buf })
135
  pcall(vim.treesitter.language.register, "bash", "curl")
136
  vim.keymap.set("n", curl.config.map_execute, curl.execute, {
137
    buffer = buf,
138
    noremap = true,
139
    silent = true,
140
    desc = "Execute curl query under cursor",
141
  })
142
  H.query_buffers[name] = buf
143
  return true
144
end
145
146
function H.get_query_buffer()
147
  local file = H.query_cache_file()
148
  if not file then return nil end
149
150
  local buf = H.query_buffers[file]
151
  if not H.is_valid_buf(buf) then
152
    buf = vim.fn.bufadd(file)
153
    vim.fn.bufload(buf)
154
    vim.api.nvim_set_option_value("bufhidden", "hide", { buf = buf })
155
    vim.api.nvim_set_option_value("swapfile", false, { buf = buf })
156
    vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
157
158
    vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "BufLeave", "VimLeavePre" }, {
159
      group = vim.api.nvim_create_augroup("curl_nvim_cache_" .. buf, { clear = true }),
160
      buffer = buf,
161
      callback = function() H.save_query_buffer(buf, file) end,
162
    })
163
  end
164
165
  H.attach_curl_buffer(buf)
166
  return buf
167
end
168
169
local function get_output_buffer()
170
  if H.is_valid_buf(H.output_buf) then return H.output_buf end
171
172
  local existing = vim.fn.bufnr "^Curl output$"
173
  if existing ~= -1 and H.is_valid_buf(existing) then
174
    H.output_buf = existing
175
  else
176
    H.output_buf = vim.api.nvim_create_buf(false, true)
177
    vim.api.nvim_buf_set_name(H.output_buf, "Curl output")
178
    vim.api.nvim_set_option_value("buftype", "nofile", { buf = H.output_buf })
179
    vim.api.nvim_set_option_value("bufhidden", "hide", { buf = H.output_buf })
180
    vim.api.nvim_set_option_value("swapfile", false, { buf = H.output_buf })
181
  end
182
183
  vim.api.nvim_set_option_value("filetype", "json", { buf = H.output_buf })
184
  vim.api.nvim_set_option_value("modifiable", false, { buf = H.output_buf })
185
  return H.output_buf
186
end
187
188
function H.find_window(buf)
189
  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
190
    if vim.api.nvim_win_get_buf(win) == buf then return win end
191
  end
192
end
193
194
function H.open_output_window(query_buf)
195
  local output_buf = get_output_buffer()
196
  local output_win = H.find_window(output_buf)
197
  if output_win and vim.api.nvim_win_is_valid(output_win) then return output_win end
198
199
  local query_win = H.find_window(query_buf) or vim.api.nvim_get_current_win()
200
  if not vim.api.nvim_win_is_valid(query_win) then return nil end
201
202
  vim.api.nvim_set_current_win(query_win)
203
  vim.cmd(curl.config.open_cmd)
204
  vim.api.nvim_win_set_buf(0, output_buf)
205
  vim.api.nvim_set_current_win(query_win)
206
  return vim.api.nvim_get_current_win()
207
end
208
209
function H.find_query_block(lines, cursor_line)
210
  if not lines[cursor_line] or vim.trim(lines[cursor_line]) == "" then return nil end
211
212
  local start_line
213
  for i = cursor_line, 1, -1 do
214
    if H.is_query_start(lines[i]) then
215
      start_line = i
216
      break
217
    end
218
    if vim.trim(lines[i]) == "" then break end
219
  end
220
  if not start_line then return nil end
221
222
  local end_line = #lines
223
  for i = start_line + 1, #lines do
224
    if H.is_query_start(lines[i]) or vim.trim(lines[i]) == "" then
225
      end_line = i - 1
226
      break
227
    end
228
  end
229
230
  if cursor_line < start_line or cursor_line > end_line then return nil end
231
  return start_line, end_line
232
end
233
234
function H.shell_split(input)
235
  local args, current, quote = {}, {}, nil
236
  local i = 1
237
  while i <= #input do
238
    local ch = input:sub(i, i)
239
    if quote == "'" then
240
      if ch == "'" then
241
        quote = nil
242
      else
243
        table.insert(current, ch)
244
      end
245
    elseif quote == '"' then
246
      if ch == '"' then
247
        quote = nil
248
      elseif ch == "\\" and i < #input then
249
        i = i + 1
250
        table.insert(current, input:sub(i, i))
251
      else
252
        table.insert(current, ch)
253
      end
254
    else
255
      if ch == "'" or ch == '"' then
256
        quote = ch
257
      elseif ch:match "%s" then
258
        if #current > 0 then
259
          table.insert(args, table.concat(current))
260
          current = {}
261
        end
262
      elseif ch == "\\" and i < #input then
263
        i = i + 1
264
        table.insert(current, input:sub(i, i))
265
      else
266
        table.insert(current, ch)
267
      end
268
    end
269
    i = i + 1
270
  end
271
272
  if quote then return nil, "unterminated quote in query" end
273
  if #current > 0 then table.insert(args, table.concat(current)) end
274
  return args
275
end
276
277
function H.build_command(query_lines)
278
  local body_parts = {}
279
  for _, line in ipairs(query_lines) do
280
    local s = vim.trim(line)
281
    if s ~= "" and not s:match "^#" then table.insert(body_parts, (s:gsub("\\%s*$", ""))) end
282
  end
283
284
  local body = vim.trim(table.concat(body_parts, " "))
285
  if body == "" then return nil, "empty query" end
286
287
  local parsed, err = H.shell_split(body)
288
  if not parsed or #parsed == 0 then return nil, err end
289
  if parsed[1] == "curl" then table.remove(parsed, 1) end
290
291
  local cmd = { curl.config.curl or "curl", "--silent", "--show-error" }
292
  vim.list_extend(cmd, curl.config.default_flags or {})
293
  vim.list_extend(cmd, parsed)
294
  return cmd
295
end
296
297
function H.write_output(query_buf, text)
298
  H.open_output_window(query_buf)
299
  local buf = get_output_buffer()
300
  local lines = vim.split(text or "", "\n", { plain = true })
301
  while #lines > 1 and lines[#lines] == "" do
302
    table.remove(lines)
303
  end
304
  if #lines == 0 then lines = { "" } end
305
306
  vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
307
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
308
  vim.api.nvim_set_option_value("modifiable", false, { buf = buf })
309
  vim.api.nvim_set_option_value("filetype", "json", { buf = buf })
310
end
311
312
function H.write_formatted_output(query_buf, output)
313
  if output == "" or vim.fn.executable "jq" ~= 1 then
314
    vim.schedule(function() H.write_output(query_buf, output) end)
315
    return
316
  end
317
318
  local lines = vim.split(output:gsub("\r\n", "\n"):gsub("\r", "\n"), "\n", { plain = true })
319
  local json_index
320
  for i, line in ipairs(lines) do
321
    if vim.trim(line):match "^[%[%{]" then
322
      json_index = i
323
      break
324
    end
325
  end
326
  if not json_index then
327
    vim.schedule(function() H.write_output(query_buf, output) end)
328
    return
329
  end
330
331
  local headers = {}
332
  for i = 1, json_index - 1 do
333
    table.insert(headers, vim.trim(lines[i]))
334
  end
335
  local json_body = table.concat(vim.list_slice(lines, json_index, #lines), "\n")
336
337
  vim.system({ "jq", "." }, { text = true, stdin = json_body }, function(result)
338
    if result.code == 0 and result.stdout and result.stdout ~= "" then
339
      output = #headers > 0 and (table.concat(headers, "\n") .. "\n" .. result.stdout) or result.stdout
340
    end
341
    vim.schedule(function() H.write_output(query_buf, output) end)
342
  end)
343
end
344
345
return curl