all repos

curl.nvim @ master

use curl within neovim

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
clanker did some bug fixing, 21 days 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
    vim.api.nvim_create_autocmd("BufDelete", {
36
      group = vim.api.nvim_create_augroup("curl_nvim_cache_cleanup", { clear = true }),
37
      pattern = "*.curl",
38
      callback = function(args)
39
        H.query_buffers[vim.api.nvim_buf_get_name(args.buf)] = nil
40
      end,
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
84
  if H.running_request then
85
    pcall(function() H.running_request:kill(15) end)
86
    H.running_request = nil
87
  end
88
89
  H.running_request = vim.system({ vim.o.shell, "-c", command }, { text = true }, function(result)
90
    H.running_request = nil
91
    local output = result.stdout or ""
92
    if result.stderr and result.stderr ~= "" then
93
      output = output == "" and result.stderr or (output .. "\n\n" .. result.stderr)
94
    end
95
    if output == "" then output = "curl exited with code " .. result.code end
96
    H.write_formatted_output(query_buf, output)
97
  end)
98
end
99
100
function H.is_valid_buf(buf) return type(buf) == "number" and buf > 0 and vim.api.nvim_buf_is_valid(buf) end
101
102
function H.is_curl_file(path) return type(path) == "string" and path ~= "" and path:sub(-5) == ".curl" end
103
104
function H.is_query_start(line)
105
  if not line then return false end
106
  return line:match "^%s*curl%s" or line:match "^%s*curl$" or line:match "^%s*https?://"
107
end
108
109
function H.save_query_buffer(buf, file)
110
  if not H.is_valid_buf(buf) then return end
111
  file = file or vim.api.nvim_buf_get_name(buf)
112
  if not file or file == "" then return end
113
  if not H.is_curl_file(file) then
114
    vim.notify("[curl.nvim] cannot save: buffer is not a .curl file", vim.log.levels.WARN)
115
    return
116
  end
117
118
  local ok = pcall(vim.fn.writefile, vim.api.nvim_buf_get_lines(buf, 0, -1, false), file, "b")
119
  if ok then
120
    pcall(vim.api.nvim_set_option_value, "modified", false, { buf = buf })
121
  else
122
    vim.notify("[curl.nvim] failed to save request file: " .. file, vim.log.levels.ERROR)
123
  end
124
end
125
126
function H.query_cache_file()
127
  if vim.fn.isdirectory(H.cache_dir) ~= 1 and vim.fn.mkdir(H.cache_dir, "p") ~= 1 then
128
    vim.notify("[curl.nvim] failed to create cache dir: " .. H.cache_dir, vim.log.levels.ERROR)
129
    return nil
130
  end
131
132
  return vim.fs.joinpath(H.cache_dir, vim.fn.sha256(vim.fn.getcwd()) .. ".curl")
133
end
134
135
function H.attach_curl_buffer(buf)
136
  if not H.is_valid_buf(buf) then return false end
137
  local name = vim.api.nvim_buf_get_name(buf)
138
  if not H.is_curl_file(name) then return false end
139
140
  vim.api.nvim_set_option_value("filetype", "curl", { buf = buf })
141
  vim.api.nvim_set_option_value("syntax", "sh", { buf = buf })
142
  vim.api.nvim_set_option_value("commentstring", "# %s", { buf = buf })
143
  pcall(vim.treesitter.language.register, "bash", "curl")
144
  vim.keymap.set("n", curl.config.map_execute, curl.execute, {
145
    buffer = buf,
146
    noremap = true,
147
    silent = true,
148
    desc = "Execute curl query under cursor",
149
  })
150
  H.query_buffers[name] = buf
151
  return true
152
end
153
154
function H.get_query_buffer()
155
  local file = H.query_cache_file()
156
  if not file then return nil end
157
158
  local buf = H.query_buffers[file]
159
  if not H.is_valid_buf(buf) then
160
    buf = vim.fn.bufadd(file)
161
    vim.fn.bufload(buf)
162
    vim.api.nvim_set_option_value("bufhidden", "hide", { buf = buf })
163
    vim.api.nvim_set_option_value("swapfile", false, { buf = buf })
164
    vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
165
166
    vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "BufLeave", "VimLeavePre" }, {
167
      group = vim.api.nvim_create_augroup("curl_nvim_cache_" .. buf, { clear = true }),
168
      buffer = buf,
169
      callback = function() H.save_query_buffer(buf, file) end,
170
    })
171
  end
172
173
  H.attach_curl_buffer(buf)
174
  return buf
175
end
176
177
local function get_output_buffer()
178
  if H.is_valid_buf(H.output_buf) then return H.output_buf end
179
180
  local existing = vim.fn.bufnr "^Curl output$"
181
  if existing ~= -1 and H.is_valid_buf(existing) then
182
    H.output_buf = existing
183
  else
184
    H.output_buf = vim.api.nvim_create_buf(false, true)
185
    vim.api.nvim_buf_set_name(H.output_buf, "Curl output")
186
    vim.api.nvim_set_option_value("buftype", "nofile", { buf = H.output_buf })
187
    vim.api.nvim_set_option_value("bufhidden", "hide", { buf = H.output_buf })
188
    vim.api.nvim_set_option_value("swapfile", false, { buf = H.output_buf })
189
  end
190
191
  vim.api.nvim_set_option_value("filetype", "json", { buf = H.output_buf })
192
  vim.api.nvim_set_option_value("modifiable", false, { buf = H.output_buf })
193
  return H.output_buf
194
end
195
196
function H.find_window(buf)
197
  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
198
    if vim.api.nvim_win_get_buf(win) == buf then return win end
199
  end
200
end
201
202
function H.open_output_window(query_buf)
203
  local output_buf = get_output_buffer()
204
  local output_win = H.find_window(output_buf)
205
  if output_win and vim.api.nvim_win_is_valid(output_win) then return output_win end
206
207
  local query_win = H.find_window(query_buf) or vim.api.nvim_get_current_win()
208
  if not vim.api.nvim_win_is_valid(query_win) then return nil end
209
210
  vim.api.nvim_set_current_win(query_win)
211
  vim.cmd(curl.config.open_cmd)
212
  vim.api.nvim_win_set_buf(0, output_buf)
213
  vim.api.nvim_set_current_win(query_win)
214
  return vim.api.nvim_get_current_win()
215
end
216
217
function H.find_query_block(lines, cursor_line)
218
  if not lines[cursor_line] or vim.trim(lines[cursor_line]) == "" then return nil end
219
220
  local start_line
221
  for i = cursor_line, 1, -1 do
222
    if H.is_query_start(lines[i]) then
223
      start_line = i
224
      break
225
    end
226
  end
227
  if not start_line then return nil end
228
229
  local end_line = #lines
230
  for i = start_line + 1, #lines do
231
    if H.is_query_start(lines[i]) then
232
      end_line = i - 1
233
      break
234
    end
235
  end
236
237
  if cursor_line < start_line or cursor_line > end_line then return nil end
238
  return start_line, end_line
239
end
240
241
function H.quote_json_bodies(lines)
242
  local result = {}
243
  local json_open
244
  local stack_depth = 0
245
  local in_json = false
246
  for _, line in ipairs(lines) do
247
    local s = line
248
    local trimmed = vim.trim(s)
249
250
    if not in_json and trimmed:match("^[%[%{]") then
251
      json_open = trimmed:sub(1, 1)
252
      in_json = true
253
      s = "'" .. s
254
    end
255
256
    if in_json then
257
      for ch in s:gmatch(".") do
258
        if ch == json_open then
259
          stack_depth = stack_depth + 1
260
        elseif (json_open == "{" and ch == "}") or (json_open == "[" and ch == "]") then
261
          stack_depth = stack_depth - 1
262
          if stack_depth == 0 then
263
            s = s .. "'"
264
            in_json = false
265
            break
266
          end
267
        end
268
      end
269
    end
270
271
    table.insert(result, s)
272
  end
273
  return result
274
end
275
276
function H.build_command(query_lines)
277
  local parts = {}
278
  for _, line in ipairs(H.quote_json_bodies(query_lines)) do
279
    local s = vim.trim(line)
280
    if s ~= "" and not s:match("^#") then
281
      table.insert(parts, (s:gsub("\\%s*$", "")))
282
    end
283
  end
284
285
  local body = vim.trim(table.concat(parts, " "))
286
  if body == "" then return nil, "empty query" end
287
288
  body = body:gsub("^curl%s+", "", 1)
289
290
  local flag_parts = {}
291
  for _, f in ipairs(curl.config.default_flags or {}) do
292
    table.insert(flag_parts, f)
293
  end
294
295
  return (curl.config.curl or "curl") .. " " .. table.concat(flag_parts, " ") .. " -sSL " .. body
296
end
297
298
function H.write_output(query_buf, text)
299
  H.open_output_window(query_buf)
300
  local buf = get_output_buffer()
301
  local lines = vim.split(text or "", "\n", { plain = true })
302
  while #lines > 1 and lines[#lines] == "" do
303
    table.remove(lines)
304
  end
305
  if #lines == 0 then lines = { "" } end
306
307
  vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
308
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
309
  vim.api.nvim_set_option_value("modifiable", false, { buf = buf })
310
  vim.api.nvim_set_option_value("filetype", "json", { buf = buf })
311
end
312
313
function H.write_formatted_output(query_buf, output)
314
  if output == "" or vim.fn.executable "jq" ~= 1 then
315
    vim.schedule(function() H.write_output(query_buf, output) end)
316
    return
317
  end
318
319
  local lines = vim.split(output:gsub("\r\n", "\n"):gsub("\r", "\n"), "\n", { plain = true })
320
  local json_index
321
  for i, line in ipairs(lines) do
322
    if vim.trim(line):match "^[%[%{]" then
323
      json_index = i
324
      break
325
    end
326
  end
327
  if not json_index then
328
    vim.schedule(function() H.write_output(query_buf, output) end)
329
    return
330
  end
331
332
  local headers = {}
333
  for i = 1, json_index - 1 do
334
    local trimmed = vim.trim(lines[i])
335
    if trimmed ~= "" then
336
      table.insert(headers, trimmed)
337
    end
338
  end
339
  local json_body = table.concat(vim.list_slice(lines, json_index, #lines), "\n")
340
341
  vim.system({ "jq", "." }, { text = true, stdin = json_body }, function(result)
342
    if result.code == 0 and result.stdout and result.stdout ~= "" then
343
      output = #headers > 0 and (table.concat(headers, "\n") .. "\n\n" .. result.stdout) or result.stdout
344
    end
345
    vim.schedule(function() H.write_output(query_buf, output) end)
346
  end)
347
end
348
349
return curl