mirror of
https://github.com/neovim/neovim.git
synced 2026-03-31 04:42:03 +00:00
feat(net): vim.net.request(outbuf) writes response to buffer #36164
Problem:
Non-trivial to write output of vim.net.request to buffer. Requires extra
code in plugin/net.lua which can't be reused by other plugin authors.
```
vim.net.request('https://neovim.io', {}, function(err, res)
if not err then
local buf = vim.api.nvim_create_buf(true, false)
if res then
local lines = vim.split(res.body, '\n', { plain = true })
vim.api.nvim_buf_set_lines(buf, 0, -1, true, lines)
end
end
end)
```
Solution:
Accept an optional `outbuf` argument to indicate the buffer to write output
to, similar to `outpath`.
vim.net.request('https://neovim.io', { outbuf = buf })
Other fixes / followups:
- Make plugin/net.lua smaller
- Return objection with close() method
- vim.net.request.Opts class
- vim.validate single calls
- Use (''):format(...) instead of `..`
This commit is contained in:
committed by
GitHub
parent
7c3df3e2ea
commit
f29b3b5d45
@@ -4149,29 +4149,62 @@ vim.mpack.encode({obj}) *vim.mpack.encode()*
|
||||
==============================================================================
|
||||
Lua module: vim.net *vim.net*
|
||||
|
||||
*vim.net.request.Response*
|
||||
|
||||
Fields: ~
|
||||
• {body} (`string`) The HTTP body of the request
|
||||
|
||||
|
||||
vim.net.request({url}, {opts}, {on_response}) *vim.net.request()*
|
||||
Makes an HTTP GET request to the given URL (asynchronous).
|
||||
|
||||
This function operates in one mode:
|
||||
• Asynchronous (non-blocking): Returns immediately and passes the response
|
||||
object to the provided `on_response` handler on completion.
|
||||
This function is asynchronous (non-blocking), returning immediately and
|
||||
passing the response object to the optional `on_response` handler on
|
||||
completion.
|
||||
|
||||
Examples: >lua
|
||||
-- Write response body to file
|
||||
vim.net.request('https://neovim.io/charter/', {
|
||||
outpath = 'vision.html',
|
||||
})
|
||||
|
||||
-- Process the response
|
||||
vim.net.request(
|
||||
'https://api.github.com/repos/neovim/neovim',
|
||||
{},
|
||||
function (err, res)
|
||||
if err then return end
|
||||
local stars = vim.json.decode(res.body).stargazers_count
|
||||
vim.print(('Neovim currently has %d stars'):format(stars))
|
||||
end
|
||||
)
|
||||
|
||||
-- Write to both file and current buffer, but cancel it
|
||||
local job = vim.net.request('https://neovim.io/charter/', {
|
||||
outpath = 'vision.html',
|
||||
outbuf = 0,
|
||||
})
|
||||
job:close()
|
||||
<
|
||||
|
||||
Parameters: ~
|
||||
• {url} (`string`) The URL for the request.
|
||||
• {opts} (`table?`) Optional parameters:
|
||||
• `verbose` (boolean|nil): Enables verbose output.
|
||||
• `retry` (integer|nil): Number of retries on transient
|
||||
• {opts} (`table?`) A table with the following fields:
|
||||
• {verbose}? (`boolean`) Enables verbose output.
|
||||
• {retry}? (`integer`) Number of retries on transient
|
||||
failures (default: 3).
|
||||
• `outpath` (string|nil): File path to save the
|
||||
response body to. If set, the `body` value in the
|
||||
Response Object will be `true` instead of the
|
||||
response body.
|
||||
• {on_response} (`fun(err?: string, response?: { body: string|boolean })`)
|
||||
• {outpath}? (`string`) File path to save the response
|
||||
body to.
|
||||
• {outbuf}? (`integer`) Buffer to save the response
|
||||
body to.
|
||||
• {on_response} (`fun(err: string?, response: vim.net.request.Response?)?`)
|
||||
Callback invoked on request completion. The `body`
|
||||
field in the response object contains the raw response
|
||||
data (text or binary). Called with (err, nil) on
|
||||
failure, or (nil, { body = string|boolean }) on
|
||||
success.
|
||||
field in the response parameter contains the raw
|
||||
response data (text or binary).
|
||||
|
||||
Return: ~
|
||||
(`{ close: fun() }`) Table with method to cancel, similar to
|
||||
|vim.SystemObj|.
|
||||
|
||||
|
||||
==============================================================================
|
||||
|
||||
@@ -26,6 +26,11 @@ EXPERIMENTS
|
||||
|
||||
• todo
|
||||
|
||||
LUA
|
||||
|
||||
• |vim.net.request()| does not set the response body to `true` if an output
|
||||
buffer is supplied.
|
||||
|
||||
LSP
|
||||
|
||||
• |vim.lsp.buf.selection_range()| now accepts an integer which specifies its
|
||||
|
||||
@@ -1,23 +1,67 @@
|
||||
local M = {}
|
||||
|
||||
---@class vim.net.request.Opts
|
||||
---@inlinedoc
|
||||
---
|
||||
---Enables verbose output.
|
||||
---@field verbose? boolean
|
||||
---
|
||||
---Number of retries on transient failures (default: 3).
|
||||
---@field retry? integer
|
||||
---
|
||||
---File path to save the response body to.
|
||||
---@field outpath? string
|
||||
---
|
||||
---Buffer to save the response body to.
|
||||
---@field outbuf? integer
|
||||
|
||||
---@class vim.net.request.Response
|
||||
---
|
||||
---The HTTP body of the request
|
||||
---@field body string
|
||||
|
||||
--- Makes an HTTP GET request to the given URL (asynchronous).
|
||||
---
|
||||
--- This function operates in one mode:
|
||||
--- - Asynchronous (non-blocking): Returns immediately and passes the response object to the
|
||||
--- provided `on_response` handler on completion.
|
||||
--- This function is asynchronous (non-blocking), returning immediately and
|
||||
--- passing the response object to the optional `on_response` handler on
|
||||
--- completion.
|
||||
---
|
||||
--- Examples:
|
||||
--- ```lua
|
||||
--- -- Write response body to file
|
||||
--- vim.net.request('https://neovim.io/charter/', {
|
||||
--- outpath = 'vision.html',
|
||||
--- })
|
||||
---
|
||||
--- -- Process the response
|
||||
--- vim.net.request(
|
||||
--- 'https://api.github.com/repos/neovim/neovim',
|
||||
--- {},
|
||||
--- function (err, res)
|
||||
--- if err then return end
|
||||
--- local stars = vim.json.decode(res.body).stargazers_count
|
||||
--- vim.print(('Neovim currently has %d stars'):format(stars))
|
||||
--- end
|
||||
--- )
|
||||
---
|
||||
--- -- Write to both file and current buffer, but cancel it
|
||||
--- local job = vim.net.request('https://neovim.io/charter/', {
|
||||
--- outpath = 'vision.html',
|
||||
--- outbuf = 0,
|
||||
--- })
|
||||
--- job:close()
|
||||
--- ```
|
||||
---
|
||||
--- @param url string The URL for the request.
|
||||
--- @param opts? table Optional parameters:
|
||||
--- - `verbose` (boolean|nil): Enables verbose output.
|
||||
--- - `retry` (integer|nil): Number of retries on transient failures (default: 3).
|
||||
--- - `outpath` (string|nil): File path to save the response body to. If set, the `body` value in the Response Object will be `true` instead of the response body.
|
||||
--- @param on_response fun(err?: string, response?: { body: string|boolean }) Callback invoked on request
|
||||
--- completion. The `body` field in the response object contains the raw response data (text or binary).
|
||||
--- Called with (err, nil) on failure, or (nil, { body = string|boolean }) on success.
|
||||
--- @param opts? vim.net.request.Opts
|
||||
--- @param on_response? fun(err: string?, response: vim.net.request.Response?)
|
||||
--- Callback invoked on request completion. The `body` field in the response
|
||||
--- parameter contains the raw response data (text or binary).
|
||||
--- @return { close: fun() } # Table with method to cancel, similar to [vim.SystemObj].
|
||||
function M.request(url, opts, on_response)
|
||||
vim.validate('url', url, 'string')
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
vim.validate('on_response', on_response, 'function')
|
||||
vim.validate('on_response', on_response, 'function', true)
|
||||
|
||||
opts = opts or {}
|
||||
local retry = opts.retry or 3
|
||||
@@ -37,19 +81,39 @@ function M.request(url, opts, on_response)
|
||||
|
||||
table.insert(args, url)
|
||||
|
||||
local function on_exit(res)
|
||||
local s = 'Request failed with exit code %d'
|
||||
local err_msg = res.code ~= 0
|
||||
and ((res.stderr ~= '' and res.stderr) or string.format(s, res.code))
|
||||
or nil
|
||||
local response = res.code == 0 and { body = opts.outpath and true or res.stdout } or nil
|
||||
local job = vim.system(args, {}, function(res)
|
||||
---@type string?, vim.net.request.Response?
|
||||
local err, response = nil, nil
|
||||
if res.signal ~= 0 then
|
||||
err = ('Request killed with signal %d'):format(res.signal)
|
||||
elseif res.code ~= 0 then
|
||||
err = res.stderr ~= '' and res.stderr or ('Request failed with exit code %d'):format(res.code)
|
||||
else
|
||||
if not (opts.outpath or opts.outbuf) then
|
||||
response = {
|
||||
body = res.stdout --[[@as string]],
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- nvim_buf_is_loaded and nvim_buf_set_lines are not allowed in fast context
|
||||
vim.schedule(function()
|
||||
if res.code == 0 and opts.outbuf and vim.api.nvim_buf_is_loaded(opts.outbuf) then
|
||||
local lines = vim.split(res.stdout, '\n', { plain = true })
|
||||
vim.api.nvim_buf_set_lines(opts.outbuf, 0, -1, true, lines)
|
||||
end
|
||||
end)
|
||||
|
||||
if on_response then
|
||||
on_response(err_msg, response)
|
||||
on_response(err, response)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
vim.system(args, {}, on_exit)
|
||||
return {
|
||||
close = function()
|
||||
job:kill('sigint')
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -39,7 +39,7 @@ augroup Network
|
||||
au!
|
||||
au BufReadCmd file://* call netrw#FileUrlEdit(expand("<amatch>"))
|
||||
au BufReadCmd ftp://*,rcp://*,scp://*,dav://*,davs://*,rsync://*,sftp://* exe "sil doau BufReadPre ".fnameescape(expand("<amatch>"))|call netrw#Nread(2,expand("<amatch>"))|exe "sil doau BufReadPost ".fnameescape(expand("<amatch>"))
|
||||
au FileReadCmd ftp://*,rcp://*,scp://*,http://*,file://*,https://*,dav://*,davs://*,rsync://*,sftp://* exe "sil doau FileReadPre ".fnameescape(expand("<amatch>"))|call netrw#Nread(1,expand("<amatch>"))|exe "sil doau FileReadPost ".fnameescape(expand("<amatch>"))
|
||||
au FileReadCmd ftp://*,rcp://*,scp://*,file://*,dav://*,davs://*,rsync://*,sftp://* exe "sil doau FileReadPre ".fnameescape(expand("<amatch>"))|call netrw#Nread(1,expand("<amatch>"))|exe "sil doau FileReadPost ".fnameescape(expand("<amatch>"))
|
||||
au BufWriteCmd ftp://*,rcp://*,scp://*,http://*,file://*,dav://*,davs://*,rsync://*,sftp://* exe "sil doau BufWritePre ".fnameescape(expand("<amatch>"))|exe 'Nwrite '.fnameescape(expand("<amatch>"))|exe "sil doau BufWritePost ".fnameescape(expand("<amatch>"))
|
||||
au FileWriteCmd ftp://*,rcp://*,scp://*,http://*,file://*,dav://*,davs://*,rsync://*,sftp://* exe "sil doau FileWritePre ".fnameescape(expand("<amatch>"))|exe "'[,']".'Nwrite '.fnameescape(expand("<amatch>"))|exe "sil doau FileWritePost ".fnameescape(expand("<amatch>"))
|
||||
try
|
||||
|
||||
@@ -1,45 +1,70 @@
|
||||
vim.g.loaded_remote_file_loader = true
|
||||
|
||||
--- Callback for BufReadCmd on remote URLs.
|
||||
--- @param ev { buf: integer }
|
||||
local function on_remote_read(ev)
|
||||
if vim.fn.executable('curl') ~= 1 then
|
||||
vim.api.nvim_echo({
|
||||
{ 'Warning: `curl` not found; remote URL loading disabled.', 'WarningMsg' },
|
||||
}, true, {})
|
||||
return true
|
||||
end
|
||||
|
||||
local bufnr = ev.buf
|
||||
local url = vim.api.nvim_buf_get_name(bufnr)
|
||||
local view = vim.fn.winsaveview()
|
||||
|
||||
vim.api.nvim_echo({ { 'Fetching ' .. url .. ' …', 'MoreMsg' } }, true, {})
|
||||
|
||||
vim.net.request(
|
||||
url,
|
||||
{ retry = 3 },
|
||||
vim.schedule_wrap(function(err, content)
|
||||
if err then
|
||||
vim.notify('Failed to fetch ' .. url .. ': ' .. tostring(err), vim.log.levels.ERROR)
|
||||
vim.fn.winrestview(view)
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.split(content.body, '\n', { plain = true })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
|
||||
vim.api.nvim_exec_autocmds('BufRead', { group = 'filetypedetect', buffer = bufnr })
|
||||
vim.bo[bufnr].modified = false
|
||||
|
||||
vim.fn.winrestview(view)
|
||||
vim.api.nvim_echo({ { 'Loaded ' .. url, 'Normal' } }, true, {})
|
||||
end)
|
||||
)
|
||||
if vim.g.loaded_nvim_net_plugin ~= nil then
|
||||
return
|
||||
end
|
||||
vim.g.loaded_nvim_net_plugin = true
|
||||
|
||||
local augroup = vim.api.nvim_create_augroup('nvim.net.remotefile', {})
|
||||
local url_patterns = { 'http://*', 'https://*' }
|
||||
|
||||
vim.api.nvim_create_autocmd('BufReadCmd', {
|
||||
group = vim.api.nvim_create_augroup('nvim.net.remotefile', {}),
|
||||
pattern = { 'http://*', 'https://*' },
|
||||
group = augroup,
|
||||
pattern = url_patterns,
|
||||
desc = 'Edit remote files (:edit https://example.com)',
|
||||
callback = on_remote_read,
|
||||
callback = function(ev)
|
||||
if vim.fn.executable('curl') ~= 1 then
|
||||
vim.notify('vim.net.request: curl not found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local url = ev.file
|
||||
vim.notify(('Fetching %s …'):format(url), vim.log.levels.INFO)
|
||||
|
||||
vim.net.request(
|
||||
url,
|
||||
{ outbuf = ev.buf },
|
||||
vim.schedule_wrap(function(err, _)
|
||||
if err then
|
||||
vim.notify(('Failed to fetch %s: %s'):format(url, err), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_exec_autocmds('BufRead', { group = 'filetypedetect', buffer = ev.buf })
|
||||
vim.bo[ev.buf].modified = false
|
||||
vim.notify(('Loaded %s'):format(url), vim.log.levels.INFO)
|
||||
end)
|
||||
)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('FileReadCmd', {
|
||||
group = augroup,
|
||||
pattern = url_patterns,
|
||||
desc = 'Read remote files (:read https://example.com)',
|
||||
callback = function(ev)
|
||||
if vim.fn.executable('curl') ~= 1 then
|
||||
vim.notify('vim.net.request: curl not found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local url = ev.file
|
||||
vim.notify(('Fetching %s …'):format(url), vim.log.levels.INFO)
|
||||
|
||||
vim.net.request(
|
||||
url,
|
||||
{},
|
||||
vim.schedule_wrap(function(err, response)
|
||||
if err or not response then
|
||||
vim.notify(('Failed to fetch %s: %s'):format(url, err), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Start inserting the response at the line number given by read (e.g. :10read).
|
||||
-- FIXME: Doesn't work for :0read as '[ is set to 1. See #7177 for possible solutions.
|
||||
local start = vim.fn.line("'[")
|
||||
local lines = vim.split(response.body or '', '\n', { plain = true })
|
||||
vim.api.nvim_buf_set_lines(ev.buf, start, start, true, lines)
|
||||
vim.notify(('Loaded %s'):format(url), vim.log.levels.INFO)
|
||||
end)
|
||||
)
|
||||
end,
|
||||
})
|
||||
|
||||
@@ -21,9 +21,8 @@ describe('vim.net.request', function()
|
||||
local content = exec_lua([[
|
||||
local done = false
|
||||
local result
|
||||
local M = require('vim.net')
|
||||
|
||||
M.request("https://httpbingo.org/anything", { retry = 3 }, function(err, body)
|
||||
vim.net.request("https://httpbingo.org/anything", { retry = 3 }, function(err, body)
|
||||
assert(not err, err)
|
||||
result = body.body
|
||||
done = true
|
||||
@@ -63,9 +62,8 @@ describe('vim.net.request', function()
|
||||
local err = exec_lua([[
|
||||
local done = false
|
||||
local result
|
||||
local M = require('vim.net')
|
||||
|
||||
M.request("https://httpbingo.org/status/404", {}, function(e, _)
|
||||
vim.net.request("https://httpbingo.org/status/404", {}, function(e, _)
|
||||
result = e
|
||||
done = true
|
||||
end)
|
||||
@@ -76,4 +74,43 @@ describe('vim.net.request', function()
|
||||
|
||||
assert_404_error(err)
|
||||
end)
|
||||
|
||||
it('plugin writes output to buffer', function()
|
||||
t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test')
|
||||
local content = exec_lua([[
|
||||
local lines
|
||||
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.net.request("https://httpbingo.org", { outbuf = buf })
|
||||
|
||||
vim.wait(2000, function()
|
||||
lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
return lines[1] ~= ""
|
||||
end)
|
||||
|
||||
return lines
|
||||
]])
|
||||
assert(content and content[1]:find('html'))
|
||||
end)
|
||||
|
||||
it('works with :read', function()
|
||||
t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test')
|
||||
local content = exec_lua([[
|
||||
vim.cmd('runtime plugin/net.lua')
|
||||
local lines
|
||||
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'Here is some text' })
|
||||
vim.cmd(':read https://example.com')
|
||||
|
||||
vim.wait(2000, function()
|
||||
lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
return #lines > 1
|
||||
end)
|
||||
|
||||
return lines
|
||||
]])
|
||||
t.eq(true, content ~= nil)
|
||||
t.eq(true, content[1]:find('Here') ~= nil)
|
||||
t.eq(true, content[2]:find('html') ~= nil)
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user