mirror of
https://github.com/neovim/neovim.git
synced 2026-08-27 17:41:48 +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*
|
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()*
|
vim.net.request({url}, {opts}, {on_response}) *vim.net.request()*
|
||||||
Makes an HTTP GET request to the given URL (asynchronous).
|
Makes an HTTP GET request to the given URL (asynchronous).
|
||||||
|
|
||||||
This function operates in one mode:
|
This function is asynchronous (non-blocking), returning immediately and
|
||||||
• Asynchronous (non-blocking): Returns immediately and passes the response
|
passing the response object to the optional `on_response` handler on
|
||||||
object to the provided `on_response` handler on completion.
|
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: ~
|
Parameters: ~
|
||||||
• {url} (`string`) The URL for the request.
|
• {url} (`string`) The URL for the request.
|
||||||
• {opts} (`table?`) Optional parameters:
|
• {opts} (`table?`) A table with the following fields:
|
||||||
• `verbose` (boolean|nil): Enables verbose output.
|
• {verbose}? (`boolean`) Enables verbose output.
|
||||||
• `retry` (integer|nil): Number of retries on transient
|
• {retry}? (`integer`) Number of retries on transient
|
||||||
failures (default: 3).
|
failures (default: 3).
|
||||||
• `outpath` (string|nil): File path to save the
|
• {outpath}? (`string`) File path to save the response
|
||||||
response body to. If set, the `body` value in the
|
body to.
|
||||||
Response Object will be `true` instead of the
|
• {outbuf}? (`integer`) Buffer to save the response
|
||||||
response body.
|
body to.
|
||||||
• {on_response} (`fun(err?: string, response?: { body: string|boolean })`)
|
• {on_response} (`fun(err: string?, response: vim.net.request.Response?)?`)
|
||||||
Callback invoked on request completion. The `body`
|
Callback invoked on request completion. The `body`
|
||||||
field in the response object contains the raw response
|
field in the response parameter contains the raw
|
||||||
data (text or binary). Called with (err, nil) on
|
response data (text or binary).
|
||||||
failure, or (nil, { body = string|boolean }) on
|
|
||||||
success.
|
Return: ~
|
||||||
|
(`{ close: fun() }`) Table with method to cancel, similar to
|
||||||
|
|vim.SystemObj|.
|
||||||
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ EXPERIMENTS
|
|||||||
|
|
||||||
• todo
|
• todo
|
||||||
|
|
||||||
|
LUA
|
||||||
|
|
||||||
|
• |vim.net.request()| does not set the response body to `true` if an output
|
||||||
|
buffer is supplied.
|
||||||
|
|
||||||
LSP
|
LSP
|
||||||
|
|
||||||
• |vim.lsp.buf.selection_range()| now accepts an integer which specifies its
|
• |vim.lsp.buf.selection_range()| now accepts an integer which specifies its
|
||||||
|
|||||||
@@ -1,23 +1,67 @@
|
|||||||
local M = {}
|
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).
|
--- Makes an HTTP GET request to the given URL (asynchronous).
|
||||||
---
|
---
|
||||||
--- This function operates in one mode:
|
--- This function is asynchronous (non-blocking), returning immediately and
|
||||||
--- - Asynchronous (non-blocking): Returns immediately and passes the response object to the
|
--- passing the response object to the optional `on_response` handler on
|
||||||
--- provided `on_response` handler on completion.
|
--- 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 url string The URL for the request.
|
||||||
--- @param opts? table Optional parameters:
|
--- @param opts? vim.net.request.Opts
|
||||||
--- - `verbose` (boolean|nil): Enables verbose output.
|
--- @param on_response? fun(err: string?, response: vim.net.request.Response?)
|
||||||
--- - `retry` (integer|nil): Number of retries on transient failures (default: 3).
|
--- Callback invoked on request completion. The `body` field in the response
|
||||||
--- - `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.
|
--- parameter contains the raw response data (text or binary).
|
||||||
--- @param on_response fun(err?: string, response?: { body: string|boolean }) Callback invoked on request
|
--- @return { close: fun() } # Table with method to cancel, similar to [vim.SystemObj].
|
||||||
--- 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.
|
|
||||||
function M.request(url, opts, on_response)
|
function M.request(url, opts, on_response)
|
||||||
vim.validate('url', url, 'string')
|
vim.validate('url', url, 'string')
|
||||||
vim.validate('opts', opts, 'table', true)
|
vim.validate('opts', opts, 'table', true)
|
||||||
vim.validate('on_response', on_response, 'function')
|
vim.validate('on_response', on_response, 'function', true)
|
||||||
|
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local retry = opts.retry or 3
|
local retry = opts.retry or 3
|
||||||
@@ -37,19 +81,39 @@ function M.request(url, opts, on_response)
|
|||||||
|
|
||||||
table.insert(args, url)
|
table.insert(args, url)
|
||||||
|
|
||||||
local function on_exit(res)
|
local job = vim.system(args, {}, function(res)
|
||||||
local s = 'Request failed with exit code %d'
|
---@type string?, vim.net.request.Response?
|
||||||
local err_msg = res.code ~= 0
|
local err, response = nil, nil
|
||||||
and ((res.stderr ~= '' and res.stderr) or string.format(s, res.code))
|
if res.signal ~= 0 then
|
||||||
or nil
|
err = ('Request killed with signal %d'):format(res.signal)
|
||||||
local response = res.code == 0 and { body = opts.outpath and true or res.stdout } or nil
|
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
|
if on_response then
|
||||||
on_response(err_msg, response)
|
on_response(err, response)
|
||||||
end
|
end
|
||||||
end
|
end)
|
||||||
|
|
||||||
vim.system(args, {}, on_exit)
|
return {
|
||||||
|
close = function()
|
||||||
|
job:kill('sigint')
|
||||||
|
end,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ augroup Network
|
|||||||
au!
|
au!
|
||||||
au BufReadCmd file://* call netrw#FileUrlEdit(expand("<amatch>"))
|
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 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 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>"))
|
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
|
try
|
||||||
|
|||||||
@@ -1,45 +1,70 @@
|
|||||||
vim.g.loaded_remote_file_loader = true
|
if vim.g.loaded_nvim_net_plugin ~= nil then
|
||||||
|
return
|
||||||
--- 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)
|
|
||||||
)
|
|
||||||
end
|
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', {
|
vim.api.nvim_create_autocmd('BufReadCmd', {
|
||||||
group = vim.api.nvim_create_augroup('nvim.net.remotefile', {}),
|
group = augroup,
|
||||||
pattern = { 'http://*', 'https://*' },
|
pattern = url_patterns,
|
||||||
desc = 'Edit remote files (:edit https://example.com)',
|
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 content = exec_lua([[
|
||||||
local done = false
|
local done = false
|
||||||
local result
|
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)
|
assert(not err, err)
|
||||||
result = body.body
|
result = body.body
|
||||||
done = true
|
done = true
|
||||||
@@ -63,9 +62,8 @@ describe('vim.net.request', function()
|
|||||||
local err = exec_lua([[
|
local err = exec_lua([[
|
||||||
local done = false
|
local done = false
|
||||||
local result
|
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
|
result = e
|
||||||
done = true
|
done = true
|
||||||
end)
|
end)
|
||||||
@@ -76,4 +74,43 @@ describe('vim.net.request', function()
|
|||||||
|
|
||||||
assert_404_error(err)
|
assert_404_error(err)
|
||||||
end)
|
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)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user