local n = require('test.functional.testnvim')() local t = require('test.testutil') local describe, it, before_each, after_each = t.describe, t.it, t.before_each, t.after_each local api = n.api local eq = t.eq local exec_capture = n.exec_capture local exec_lua = n.exec_lua local feed = n.feed local fn = n.fn local poke_eventloop = n.poke_eventloop local fixtures = vim.fs.joinpath(t.paths.test_source_path, 'test/functional/fixtures/zip') local old_samples = vim.fs.joinpath(t.paths.test_source_path, 'test/old/testdir/samples') local function lines() return api.nvim_buf_get_lines(0, 0, -1, false) end local function edit(path) api.nvim_cmd({ cmd = 'edit', args = { path }, magic = { file = false, bar = false } }, {}) end local function clear_zip() n.clear({ args = { '--clean' } }) end local function copy_fixture(source, target) assert(vim.uv.fs_copyfile(source, target)) end local function line_of(text) for i, line in ipairs(lines()) do if line == text then return i end end error(('missing line %q in %s'):format(text, vim.inspect(lines()))) end describe('nvim.zip', function() local root before_each(function() t.skip(vim.fn.executable('unzip') == 0, 'unzip not available') root = vim.fs.normalize(t.tmpname(false) .. ' space%#') t.mkdir(root) end) after_each(function() n.rmdir(root) end) it('uses zip.lua by default', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) eq('folder/', lines()[1]) eq(true, exec_lua('return vim.g.loaded_nvim_zip_plugin == true')) eq(false, exec_lua('return vim.g.loaded_zipPlugin ~= nil')) end) it('defers to zipPlugin.vim loaded before startup plugins', function() local archive = vim.fs.joinpath(root, 'legacy.zip') copy_fixture(vim.fs.joinpath(old_samples, 'test.zip'), archive) n.clear({ args = { '--clean', '--cmd', 'packadd old-zip' } }) edit(archive) eq(true, lines()[1]:find('" zip.vim version', 1, true) ~= nil) eq(0, fn.exists('#nvim.zip')) eq(false, exec_lua('return vim.g.loaded_nvim_zip_plugin ~= nil')) end) it('yields to zipPlugin.vim loaded after startup', function() local archive = vim.fs.joinpath(root, 'legacy.zip') copy_fixture(vim.fs.joinpath(old_samples, 'test.zip'), archive) clear_zip() exec_lua([[vim.cmd.packadd('old-zip')]]) edit(archive) eq(true, lines()[1]:find('" zip.vim version', 1, true) ~= nil) eq(1, fn.exists('#nvim.zip')) eq(true, exec_lua('return vim.g.loaded_nvim_zip_plugin == true')) end) it('can be disabled', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) n.clear({ args = { '--clean', '--cmd', 'let g:loaded_nvim_zip_plugin = 1' }, }) edit(archive) eq(0, fn.exists('#nvim.zip')) eq('', api.nvim_get_option_value('filetype', { buf = 0 })) end) it('can source zip.lua repeatedly', function() clear_zip() eq( 2, exec_lua(function() vim.cmd.runtime('plugin/zip.lua') vim.cmd.runtime('plugin/zip.lua') local ids = {} for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ group = 'nvim.zip' })) do ids[autocmd.id] = true end return vim.tbl_count(ids) end) ) end) it('opens zip-compatible file types', function() local archive = vim.fs.joinpath(root, 'browser.jar') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) eq('folder/', lines()[1]) eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) end) it('browses directories and opens entries', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) eq( { 'folder/', 'inner.zip', '../escape.txt', '/absolute.txt', 'crlf.txt', 'noeol.txt' }, lines() ) eq('zip', api.nvim_get_option_value('filetype', { buf = 0 })) eq(true, exec_capture('syntax list zipDirectory'):find('zipDirectory', 1, true) ~= nil) feed('') poke_eventloop() eq({ 'nested/', 'root.txt', 'root.java' }, lines()) feed('') poke_eventloop() eq({ 'file.txt' }, lines()) feed('-') poke_eventloop() eq({ 'nested/', 'root.txt', 'root.java' }, lines()) eq('nested/', api.nvim_get_current_line()) api.nvim_win_set_cursor(0, { 2, 0 }) feed('') poke_eventloop() eq({ 'root text' }, lines()) eq('nowrite', api.nvim_get_option_value('buftype', { buf = 0 })) eq(true, api.nvim_get_option_value('readonly', { buf = 0 })) eq(false, api.nvim_get_option_value('modifiable', { buf = 0 })) eq(false, api.nvim_get_option_value('swapfile', { buf = 0 })) end) it('opens the containing directory from the archive root', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) feed('-') poke_eventloop() eq(root, vim.fs.normalize(api.nvim_buf_get_name(0))) eq('browser.zip', api.nvim_get_current_line()) eq('directory', api.nvim_get_option_value('filetype', { buf = 0 })) end) it('opens entries at quickfix locations', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() local uri = ('zipfile://%s::crlf.txt'):format(archive) fn.setqflist({}, 'r', { items = { { filename = uri, lnum = 2, col = 1 } }, }) api.nvim_cmd({ cmd = 'cfirst' }, {}) eq(uri, api.nvim_buf_get_name(0)) eq({ 2, 0 }, api.nvim_win_get_cursor(0)) eq('text', api.nvim_get_option_value('filetype', { buf = 0 })) end) it('does not reinterpret an entry ending in .zip as an archive', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) api.nvim_win_set_cursor(0, { line_of('inner.zip'), 0 }) feed('') poke_eventloop() eq({ 'nested payload' }, lines()) eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) end) it('preserves normal file reading details for entries', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(('zipfile://%s::crlf.txt'):format(archive)) eq({ 'one', 'two' }, lines()) eq('dos', api.nvim_get_option_value('fileformat', { buf = 0 })) eq(true, api.nvim_get_option_value('endofline', { buf = 0 })) edit(('zipfile://%s::noeol.txt'):format(archive)) eq({ 'no final newline' }, lines()) eq(false, api.nvim_get_option_value('endofline', { buf = 0 })) end) it('integrates Java sources with zip.lua and zipPlugin.vim', function() local archive = vim.fs.joinpath(root, 'source.jar') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) for _, legacy in ipairs({ false, true }) do if legacy then n.clear({ args = { '--clean', '--cmd', 'packadd old-zip' }, }) else clear_zip() end exec_lua('vim.g.ftplugin_java_source_path = ...', archive) api.nvim_buf_set_name(0, vim.fs.joinpath(root, 'Test.java')) api.nvim_buf_set_lines(0, 0, -1, false, { 'folder.root' }) api.nvim_cmd({ cmd = 'setfiletype', args = { 'java' } }, {}) api.nvim_cmd({ cmd = 'runtime', args = { 'ftplugin/java.vim' } }, {}) feed('gf') poke_eventloop() eq(('zipfile://%s::folder/root.java'):format(archive), api.nvim_buf_get_name(0)) eq({ 'class root {}' }, lines()) eq('java', api.nvim_get_option_value('filetype', { buf = 0 })) end end) it('reads entry selectors literally', function() t.skip(t.is_os('win'), 'N/A: archive contains backslashes in entry paths') local archive = vim.fs.joinpath(root, 'special.zip') copy_fixture(vim.fs.joinpath(old_samples, 'testa.zip'), archive) clear_zip() local cases = { { 'zipglob/a[a].txt', 'a test file with []' }, { 'zipglob/a*.txt', 'a test file with a*' }, { 'zipglob/a?.txt', 'a test file with a?' }, { [[zipglob/a\.txt]], [[a test file with a\]] }, { [[zipglob/a\\.txt]], [[a test file with a double \]] }, } for _, case in ipairs(cases) do edit(('zipfile://%s::%s'):format(archive, case[1])) eq({ case[2] }, lines()) end end) it('treats archive glob characters literally', function() t.skip(t.is_os('win'), 'N/A: Windows filenames cannot contain these characters') local archive = vim.fs.joinpath(root, 'archive::[*?].zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) copy_fixture(vim.fs.joinpath(old_samples, 'test.zip'), vim.fs.joinpath(root, 'archivex.zip')) clear_zip() edit(archive) eq('folder/', lines()[1]) api.nvim_win_set_cursor(0, { line_of('inner.zip'), 0 }) feed('') poke_eventloop() eq({ 'nested payload' }, lines()) local uri = ('zipfile://%s::crlf.txt'):format(archive) edit(uri) eq(uri, api.nvim_buf_get_name(0)) eq({ 'one', 'two' }, lines()) end) it('keeps suspicious entry paths visible and readable', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) api.nvim_win_set_cursor(0, { line_of('../escape.txt'), 0 }) feed('') poke_eventloop() eq({ 'escape payload' }, lines()) end) it('opens non-zip files normally', function() local archive = vim.fs.joinpath(root, 'plain.zip') t.write_file(archive, 'plain text', true) clear_zip() edit(archive) eq({ 'plain text' }, lines()) eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) end) it('keeps the current level when reload fails', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() edit(archive) feed('') poke_eventloop() eq({ 'nested/', 'root.txt', 'root.java' }, lines()) assert(os.remove(archive)) feed('R') poke_eventloop() eq({ 'nested/', 'root.txt', 'root.java' }, lines()) copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) feed('R') poke_eventloop() eq({ 'nested/', 'root.txt', 'root.java' }, lines()) end) it('reports an unavailable backend without claiming the buffer', function() local archive = vim.fs.joinpath(root, 'browser.zip') copy_fixture(vim.fs.joinpath(fixtures, 'browser.zip'), archive) clear_zip() exec_lua([[vim.env.PATH = '']]) edit(archive) poke_eventloop() eq(true, exec_capture('messages'):find('unzip executable not found', 1, true) ~= nil) eq(false, exec_lua('return vim.b.nvim_dir ~= nil')) end) end)