refactor(zip)!: address entries with zip:// paths

Problem:
`zipfile://{archive}::{entry}` is ambiguous: `::` is legal in both an
archive path and an entry path, so the separator cannot be identified.
Splitting at the last `::` reads archives correctly but breaks entries
that contain it, and the plugin then emits buffer names it cannot read
back.

Solution:
Address entries as `zip://{archive}/{entry}`, joining the two paths.
Resolve the split by walking components: the first one that is a regular
file is the archive, because a regular file cannot have children on disk.
Entry paths may then contain any character, and no escaping is needed.

zipPlugin.vim keeps its own scheme, so the Java ftplugin emits whichever
form matches the active plugin until the legacy package is removed.
This commit is contained in:
Barrett Ruth
2026-07-31 23:52:07 -05:00
parent 8787b8b9b6
commit 886a270e7f
5 changed files with 47 additions and 35 deletions

View File

@@ -307,33 +307,36 @@ local function read_tempfile(buf, temp)
set_readonly(buf)
end
--- Parse a `zipfile://` buffer name, as used by quickfix and direct `:edit`.
---@param name string `zipfile://{archive}::{path}`
--- Resolve a `zip://` buffer name, as used by quickfix and direct `:edit`.
---
--- The archive path and the entry path are simply joined, so the split is found by walking
--- components: the first one that is a regular file is the archive, because a regular file
--- cannot have children on disk. Entry paths may therefore contain any character.
---@param name string `zip://{archive}/{path}`
---@return string?, string? archive and entry path
local function parse_uri(name)
if not vim.startswith(name, 'zipfile://') then
local function resolve_uri(name)
if not vim.startswith(name, 'zip://') then
return
end
local value = name:sub(11)
local separator ---@type integer?
local value = name:sub(7)
local offset = 1
while true do
local next_separator = value:find('::', offset, true)
if not next_separator then
break
local separator = value:find('/', offset + 1, true)
if not separator then
return
end
separator = next_separator
offset = next_separator + 2
local archive = value:sub(1, separator - 1)
local stat = uv.fs_stat(archive)
if stat and stat.type == 'file' then
return archive, value:sub(separator + 1)
end
offset = separator
end
if not separator then
return
end
return value:sub(1, separator - 1), value:sub(separator + 2)
end
---@class (private) nvim.zip.State
---@field source string Path to the archive.
---@field path? string Archive path shown by a `zipfile://` buffer.
---@field path? string Archive path shown by a `zip://` buffer.
---@field paths? string[] Entry paths carried over from the initial listing.
---@field prefix? string Archive directory currently listed.
---@field pending_prefix? string Prefix to commit once the backend succeeds.
@@ -388,13 +391,13 @@ end
--- Read one archive entry into a read-only buffer.
---@param buf integer Target entry buffer.
---@param name string `zipfile://` buffer name.
---@param name string `zip://` buffer name.
function M.read(buf, name)
buf = vim._resolve_bufnr(buf)
local state = get_state(buf)
local source, path = state and state.source, state and state.path
if not source or not path then
source, path = parse_uri(name)
source, path = resolve_uri(name)
end
if not source or not path then
set_readonly(buf)
@@ -480,7 +483,7 @@ function M.open(buf, name, entry)
require('nvim.dir').open(buf, name, M)
return
end
local uri = ('zipfile://%s::%s'):format(state.source, path)
local uri = ('zip://%s/%s'):format(state.source, path)
local entry_buf = vim.fn.bufadd(uri)
set_state(entry_buf, { source = state.source, path = path })
api.nvim_cmd({