feat(net): vim.net.request(), :edit [url] #34140

Problem:
Nvim depends on netrw to download/request URL contents.

Solution:
- Add `vim.net.request()` as a thin curl wrapper:
  - Basic GET with --silent, --show-error, --fail, --location, --retry
  - Optional `opts.outpath` to save to a file
  - Operates asynchronously. Pass an `on_response` handler to get the result.
- Add integ tests (requires NVIM_TEST_INTEG to be set) to test success
  and 404 failure.
- Health check for missing `curl`.
- Handle `:edit https://…` using `vim.net.request()`.

API Usage:
1. Asynchronous request:

    vim.net.request('https://httpbingo.org/get', { retry = 2 }, function(err, response)
      if err then
        print('Fetch failed:', err)
      else
        print('Got body of length:', #response.body)
      end
    end)

2. Download to file:

    vim.net.request('https://httpbingo.org/get', { outpath = 'out_async.txt' }, function(err)
      if err then print('Error:', err) end
    end)

3. Remote :edit integration (in runtime/plugin/net.lua) fetches into buffer:

    :edit https://httpbingo.org/get
This commit is contained in:
Tom Ampuero
2025-07-13 21:43:11 +01:00
committed by GitHub
parent 444a8b3ec6
commit 7cd5356a6f
10 changed files with 269 additions and 1 deletions

View File

@ -41,6 +41,7 @@ for k, v in pairs({
snippet = true,
pack = true,
_watch = true,
net = true,
}) do
vim._submodules[k] = v
end

View File

@ -428,6 +428,72 @@ local function check_external_tools()
else
health.warn('git not available (required by `vim.pack`)')
end
if vim.fn.executable('curl') == 1 then
local curl_path = vim.fn.exepath('curl')
local curl_job = vim.system({ curl_path, '--version' }):wait()
if curl_job.code == 0 then
local curl_out = curl_job.stdout
if not curl_out or curl_out == '' then
health.warn(
string.format('`%s --version` produced no output', curl_path),
{ curl_job.stderr }
)
return
end
local curl_version = vim.version.parse(curl_out)
if not curl_version then
health.warn('Unable to parse curl version from `curl --version`')
return
end
if vim.version.le(curl_version, { 7, 12, 3 }) then
health.warn('curl version %s not compatible', curl_version)
return
end
local lines = { string.format('curl %s (%s)', curl_version, curl_path) }
for line in vim.gsplit(curl_out, '\n', { plain = true }) do
if line ~= '' and not line:match('^curl') then
table.insert(lines, line)
end
end
-- Add subtitle only if any env var is present
local added_env_header = false
for _, var in ipairs({
'curl_ca_bundle',
'curl_home',
'curl_ssl_backend',
'ssl_cert_dir',
'ssl_cert_file',
'https_proxy',
'http_proxy',
'all_proxy',
'no_proxy',
}) do
---@type string?
local val = vim.env[var] or vim.env[var:upper()]
if val then
if not added_env_header then
table.insert(lines, 'curl-related environment variables:')
added_env_header = true
end
local shown_var = vim.env[var] and var or var:upper()
table.insert(lines, string.format(' %s=%s', shown_var, val))
end
end
health.ok(table.concat(lines, '\n'))
else
health.warn('curl is installed but failed to run `curl --version`', { curl_job.stderr })
end
else
health.error('curl not found', {
'Required for vim.net.request() to function.',
'Install curl using your package manager.',
})
end
end
function M.check()

63
runtime/lua/vim/net.lua Normal file
View File

@ -0,0 +1,63 @@
local M = {}
--- 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 completetion.
---
--- @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
--- completetion. 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)
vim.validate({
url = { url, 'string' },
opts = { opts, 'table', true },
on_response = { on_response, 'function' },
})
opts = opts or {}
local retry = opts.retry or 3
-- Build curl command
local args = { 'curl' }
if opts.verbose then
table.insert(args, '--verbose')
else
vim.list_extend(args, { '--silent', '--show-error', '--fail' })
end
vim.list_extend(args, { '--location', '--retry', tostring(retry) })
if opts.outpath then
vim.list_extend(args, { '--output', opts.outpath })
end
table.insert(args, url)
local function on_exit(res)
local err_msg = nil
local response = nil
if res.code ~= 0 then
err_msg = (res.stderr ~= '' and res.stderr)
or string.format('Request failed with exit code %d', res.code)
else
response = {
body = opts.outpath and true or res.stdout,
}
end
if on_response then
on_response(err_msg, response)
end
end
vim.system(args, {}, on_exit)
end
return M