all repos

curl.nvim @ a695cb75603c19365b07d120459ce07323a653ef

use curl within neovim

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
updaties, 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 = nil,
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()
26
      curl.open()
27
    end, { desc = "Open curl.nvim query buffer" })
28
    H.command_created = true
29
  end
30
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
47
end
48
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
58
end
59
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
68
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
91
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)
101
end
102
103
function H.is_valid_buf(buf)
104
  return type(buf) == "number" and buf > 0 and vim.api.nvim_buf_is_valid(buf)
105
end
106
107
function H.is_curl_file(path)
108
  return type(path) == "string" and path ~= "" and path:sub(-5) == ".curl"
109
end
110
111
function H.is_query_start(line)
112
  if not line then return false end
113
  return line:match "^%s*curl%s" or line:match "^%s*curl$" or line:match "^%s*https?://"
114
end
115
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
120
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
127
end
128
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
134
135
  local cwd = vim.fn.getcwd()
136
  local base = vim.fn.fnamemodify(cwd, ":t")
137
  if base == "" then base = "root" end
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
158
end
159
160
function H.get_query_buffer()
161
  local file = H.query_cache_file()
162
  if not file then return nil end
163
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 })
171
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
    })
179
  end
180
181
  H.attach_curl_buffer(buf)
182
  return buf
183
end
184
185
local function get_output_buffer()
186
  if H.is_valid_buf(H.output_buf) then return H.output_buf end
187
188
  local existing = vim.fn.bufnr "^Curl output$"
189
  if existing ~= -1 and H.is_valid_buf(existing) then
190
    H.output_buf = existing
191
  else
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 })
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
202
end
203
204
function H.find_window(buf)
205
  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
206
    if vim.api.nvim_win_get_buf(win) == buf then return win end
207
  end
208
end
209
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
214
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()
223
end
224
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
227
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
232
      break
233
    end
234
    if vim.trim(lines[i]) == "" then break end
235
  end
236
  if not start_line then return nil end
237
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
244
  end
245
246
  if cursor_line < start_line or cursor_line > end_line then return nil end
247
  return start_line, end_line
248
end
249
250
function H.shell_split(input)
251
  local args, current, quote = {}, {}, nil
252
  local i = 1
253
  while i <= #input do
254
    local ch = input:sub(i, i)
255
    if quote == "'" then
256
      if ch == "'" then
257
        quote = nil
258
      else
259
        table.insert(current, ch)
260
      end
261
    elseif quote == '"' then
262
      if ch == '"' then
263
        quote = nil
264
      elseif ch == "\\" and i < #input then
265
        i = i + 1
266
        table.insert(current, input:sub(i, i))
267
      else
268
        table.insert(current, ch)
269
      end
270
    else
271
      if ch == "'" or ch == '"' then
272
        quote = ch
273
      elseif ch:match "%s" then
274
        if #current > 0 then
275
          table.insert(args, table.concat(current))
276
          current = {}
277
        end
278
      elseif ch == "\\" and i < #input then
279
        i = i + 1
280
        table.insert(current, input:sub(i, i))
281
      else
282
        table.insert(current, ch)
283
      end
284
    end
285
    i = i + 1
286
  end
287
288
  if quote then return nil, "unterminated quote in query" end
289
  if #current > 0 then table.insert(args, table.concat(current)) end
290
  return args
291
end
292
293
function H.build_command(query_lines)
294
  local body_parts = {}
295
  for _, line in ipairs(query_lines) do
296
    local s = vim.trim(line)
297
    if s ~= "" and not s:match "^#" then table.insert(body_parts, s:gsub("\\%s*$", "")) end
298
  end
299
300
  local body = vim.trim(table.concat(body_parts, " "))
301
  if body == "" then return nil, "empty query" end
302
303
  local parsed, err = H.shell_split(body)
304
  if not parsed or #parsed == 0 then return nil, err end
305
  if parsed[1] == "curl" then table.remove(parsed, 1) end
306
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
311
end
312
313
function H.write_output(text, is_json)
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
319
  if #lines == 0 then lines = { "" } end
320
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 })
325
end
326
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)
332
    return
333
  end
334
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
342
  end
343
  if not json_index then
344
    vim.schedule(function()
345
      H.write_output(output, false)
346
    end)
347
    return
348
  end
349
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")
355
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)
366
  end)
367
end
368
369
return curl