diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffc3050..e0426d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI env: TRIPTYCH_DIR: .local/share/nvim/site/pack/simonmclean/start/triptych LUA_LS_LOG_PATH: /home/runner/lua-language-server/logs - LATEST_NVIM_VERSION: "0.11.3" + LATEST_NVIM_VERSION: "0.12.1" on: workflow_dispatch: @@ -42,7 +42,6 @@ jobs: - name: install neovim plugins run: | git config --global advice.detachedHead false - git clone https://github.com/nvim-lua/plenary.nvim.git $HOME/.local/share/nvim/site/pack/plenary/start/plenary git clone https://github.com/nvim-tree/nvim-web-devicons $HOME/.local/share/nvim/site/pack/nvim-tree/start/nvim-web-devicons git clone --recurse-submodules https://github.com/$GITHUB_REPOSITORY $HOME/$TRIPTYCH_DIR cd $HOME/$TRIPTYCH_DIR diff --git a/README.md b/README.md index 22e3fcf..d260299 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ You only ever control or focus the middle window. ## ⚡️ Requirements - Neovim >= 0.9.0 -- [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim) - Optional, if you want fancy icons - [nvim-tree/nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) - A [Nerd Font](https://www.nerdfonts.com/) @@ -54,7 +53,6 @@ Example using [Lazy](https://github.com/folke/lazy.nvim). { 'simonmclean/triptych.nvim', dependencies = { - 'nvim-lua/plenary.nvim', -- required 'nvim-tree/nvim-web-devicons', -- optional for icons 'antosha417/nvim-lsp-file-operations' -- optional LSP integration }, diff --git a/doc/triptych.nvim.txt b/doc/triptych.nvim.txt index 96bf20e..892c14d 100644 --- a/doc/triptych.nvim.txt +++ b/doc/triptych.nvim.txt @@ -44,7 +44,6 @@ FEATURES *triptych.nvim-features* REQUIREMENTS *triptych.nvim-requirements* - Neovim >= 0.9.0 -- nvim-lua/plenary.nvim - Optional, if you want fancy icons - nvim-tree/nvim-web-devicons - A Nerd Font @@ -60,7 +59,6 @@ Example using Lazy . { 'simonmclean/triptych.nvim', dependencies = { - 'nvim-lua/plenary.nvim', -- required 'nvim-tree/nvim-web-devicons', -- optional for icons 'antosha417/nvim-lsp-file-operations' -- optional LSP integration }, diff --git a/lua/triptych/actions.lua b/lua/triptych/actions.lua index 459f133..b77de09 100644 --- a/lua/triptych/actions.lua +++ b/lua/triptych/actions.lua @@ -1,7 +1,6 @@ local u = require 'triptych.utils' -local float = require 'triptych.float' local view = require 'triptych.view' -local plenary_path = require 'plenary.path' +local float = require 'triptych.float' local triptych_help = require 'triptych.help' local autocmds = require 'triptych.autocmds' local log = require 'triptych.logger' @@ -45,7 +44,51 @@ local function rename_node_and_publish(from, to) end end ----Wraps plenary Path:copy with public events +---Recursively copy a file or directory to a destination path. +---Returns a flat list of all file paths that were created. +---@param src string +---@param dest string +---@return string[] created_files +local function copy_recursive(src, dest) + local created = {} + local src_type = vim.fn.getftype(src) + + if src_type == 'dir' then + vim.fn.mkdir(dest, 'p') + local handle = vim.loop.fs_scandir(src) + if handle then + while true do + local name, _ = vim.loop.fs_scandir_next(handle) + if not name then + break + end + local sub_created = copy_recursive(src .. '/' .. name, dest .. '/' .. name) + for _, p in ipairs(sub_created) do + table.insert(created, p) + end + end + end + else + -- Read source file and write to destination + local f_in = io.open(src, 'rb') + if f_in then + local data = f_in:read '*a' + f_in:close() + local dest_dir = vim.fn.fnamemodify(dest, ':h') + vim.fn.mkdir(dest_dir, 'p') + local f_out = io.open(dest, 'wb') + if f_out then + f_out:write(data) + f_out:close() + table.insert(created, dest) + end + end + end + + return created +end + +---Copy a node (file or directory) to destination, publishing events for created files. ---Note: It only publishes events for files, not folders. This is probably fine for LSP purposes ---@param target PathDetails ---@param destination string @@ -56,29 +99,7 @@ local function duplicate_node_and_publish(target, destination) autocmds.publish_will_create_node(destination) end - local p = plenary_path:new(target.path) - -- Note: Plenary has a bug whereby a copying a directory into itself creates hundreds of nested copies - -- https://github.com/nvim-lua/plenary.nvim/pull/358 - local results = p:copy { - destination = destination, - recursive = true, - override = false, - interactive = true, - } - - local files_created = {} - - local function handle_results(results) - for key, value in pairs(results) do - if type(value) == 'table' then - handle_results(value) - elseif value then - table.insert(files_created, key.filename) - end - end - end - - handle_results(results) + local files_created = copy_recursive(target.path, destination) -- Sorting to avoid flakey test table.sort(files_created) diff --git a/lua/triptych/fs.lua b/lua/triptych/fs.lua index e0b2f88..b89ff22 100644 --- a/lua/triptych/fs.lua +++ b/lua/triptych/fs.lua @@ -1,7 +1,4 @@ local u = require 'triptych.utils' -local plenary_filetype = require 'plenary.filetype' -local plenary_path = require 'plenary.path' -local plenary_async = require 'plenary.async' local M = {} @@ -15,28 +12,53 @@ end ---@param path string ---@return string? function M.get_filetype_from_path(path) - -- plenary locks up when trying to read a fifo file, so we're sniffing this out first + -- Bail out early for fifo files to avoid hangs if vim.fn.getftype(path) == 'fifo' then return 'fifo' end - -- We still want to use plenary though, because it has more advanced filetype detection - local success, result = pcall(plenary_filetype.detect, path) - if success then + local success, result = pcall(vim.filetype.match, { filename = path }) + if success and result then return result end + -- Fallback: read a small chunk to sniff the filetype by content + local f = io.open(path, 'r') + if f then + local sample = f:read(512) or '' + f:close() + local ok, ft = pcall(vim.filetype.match, { filename = path, contents = vim.split(sample, '\n') }) + if ok and ft then + return ft + end + end end -M.read_file_async = plenary_async.wrap(function(file_path, callback) - local file = plenary_path:new(file_path) +---Read a file asynchronously and call callback(err, lines) +---@param file_path string +---@param callback fun(err: string|nil, lines: string[]|nil) +function M.read_file_async(file_path, callback) + local uv = vim.uv or vim.loop - if not file:exists() then - return callback('File does not exist', nil) - end + uv.fs_open(file_path, 'r', 438, function(open_err, fd) + if open_err or not fd then + return callback('Could not open file: ' .. (open_err or 'unknown error'), nil) + end - file:read(function(content) - callback(nil, u.multiline_str_to_table(content)) + uv.fs_fstat(fd, function(stat_err, stat) + if stat_err or not stat then + uv.fs_close(fd, function() end) + return callback('Could not stat file: ' .. (stat_err or 'unknown error'), nil) + end + + uv.fs_read(fd, stat.size, 0, function(read_err, data) + uv.fs_close(fd, function() end) + if read_err then + return callback('Could not read file: ' .. read_err, nil) + end + callback(nil, u.multiline_str_to_table(data)) + end) + end) end) -end, 2) +end ---Keep recursively reading into sub-directories, so long as each sub-directory contains only a single directory and no files ---@param path string diff --git a/lua/triptych/init.lua b/lua/triptych/init.lua index d1d5978..0da1c91 100644 --- a/lua/triptych/init.lua +++ b/lua/triptych/init.lua @@ -149,12 +149,6 @@ local function setup(user_config) return warn 'triptych.nvim requires Neovim >= 0.9.0' end - local plenary_installed, _ = pcall(require, 'plenary') - - if not plenary_installed then - return warn 'triptych.nvim requires plenary.nvim' - end - vim.g.triptych_is_open = false vim.api.nvim_create_user_command('Triptych', function(opts) diff --git a/lua/triptych/syntax_highlighting.lua b/lua/triptych/syntax_highlighting.lua index caf173b..316d2bf 100644 --- a/lua/triptych/syntax_highlighting.lua +++ b/lua/triptych/syntax_highlighting.lua @@ -28,8 +28,8 @@ M.start = function(buf, filetype) if lang then local success, _ = pcall(vim.treesitter.get_parser, buf, lang) if success then - vim.treesitter.start(buf, lang) - treesitter_applied = true + local start_success, _ = pcall(vim.treesitter.start, buf, lang) + treesitter_applied = start_success end end if not treesitter_applied then diff --git a/lua/triptych/view.lua b/lua/triptych/view.lua index dd2fcc5..59d1094 100644 --- a/lua/triptych/view.lua +++ b/lua/triptych/view.lua @@ -5,7 +5,6 @@ local fs = require 'triptych.fs' local git = require 'triptych.git' local diagnostics = require 'triptych.diagnostics' local autocmds = require 'triptych.autocmds' -local plenary_async = require 'plenary.async' local syntax_highlighting = require 'triptych.syntax_highlighting' local M = {} @@ -54,14 +53,18 @@ end ---@param child_win_buf number ---@param path string local function read_file_and_publish(child_win_buf, path) - plenary_async.run(function() - fs.read_file_async(path, function(err, lines) - if err then + fs.read_file_async(path, function(err, lines) + if err then + vim.schedule(function() vim.notify(err, vim.log.levels.ERROR) - else - autocmds.send_file_read(child_win_buf, path, lines) + end) + else + if lines then + vim.schedule(function() + autocmds.send_file_read(child_win_buf, path, lines) + end) end - end) + end end) end diff --git a/test_framework/queue.lua b/test_framework/queue.lua index b276722..21ce6e9 100644 --- a/test_framework/queue.lua +++ b/test_framework/queue.lua @@ -91,7 +91,9 @@ function TestQueue:handle_all_tests_succeeded() .. result_count.skipped .. ' skipped, ' .. result_count.passed - .. ' passed, 0 failed' + .. ' passed, ' + .. result_count.failed + .. ' failed' ) self:cleanup() diff --git a/test_framework/test.lua b/test_framework/test.lua index e916008..2dd668d 100644 --- a/test_framework/test.lua +++ b/test_framework/test.lua @@ -154,4 +154,42 @@ M.describe = function(description, tests) end end +---Deep-equality assertion. +---@param expected any +---@param actual any +---@param msg? string +M.assert_same = function(expected, actual, msg) + local function deep_eq(a, b) + if type(a) ~= type(b) then + return false + end + if type(a) ~= 'table' then + return a == b + end + for k, v in pairs(a) do + if not deep_eq(v, b[k]) then + return false + end + end + for k in pairs(b) do + if a[k] == nil then + return false + end + end + return true + end + + if not deep_eq(expected, actual) then + local lines = {} + if msg then + table.insert(lines, msg) + end + table.insert(lines, 'Expected:') + table.insert(lines, vim.inspect(expected)) + table.insert(lines, 'Got:') + table.insert(lines, vim.inspect(actual)) + error(table.concat(lines, '\n'), 2) + end +end + return M diff --git a/test_framework/utils.lua b/test_framework/utils.lua index 28ae017..01e660c 100644 --- a/test_framework/utils.lua +++ b/test_framework/utils.lua @@ -63,7 +63,7 @@ function M.raise_error(error_message) if M.is_headless() then M.print(error_message) else - error(error_message) + vim.notify(error_message, vim.log.levels.ERROR) end end diff --git a/tests/specs/config_spec.lua b/tests/specs/config_spec.lua index 000eef9..9444363 100644 --- a/tests/specs/config_spec.lua +++ b/tests/specs/config_spec.lua @@ -1,9 +1,9 @@ -local assert = require 'luassert' local u = require 'tests.utils' local config = require 'triptych.config' local framework = require 'test_framework.test' local it = framework.test local describe = framework.describe +local assert_same = framework.assert_same local function expected_default_config() return { @@ -80,7 +80,7 @@ end describe('create_merged_config', { it('returns the default config when user config is empty', function() - assert.same(expected_default_config(), config.create_merged_config {}) + assert_same(expected_default_config(), config.create_merged_config {}) end), it('merges partial user config with the default', function() @@ -99,6 +99,6 @@ describe('create_merged_config', { result.git_signs.enabled = false return result end) - assert.same(expected, config.create_merged_config(user_config)) + assert_same(expected, config.create_merged_config(user_config)) end), }) diff --git a/tests/specs/help_spec.lua b/tests/specs/help_spec.lua index efb94c2..db4a9a1 100644 --- a/tests/specs/help_spec.lua +++ b/tests/specs/help_spec.lua @@ -1,14 +1,14 @@ local help = require 'triptych.help' -local assert = require 'luassert' local framework = require 'test_framework.test' local it = framework.test local describe = framework.describe +local assert_same = framework.assert_same describe('help_lines', { it('returns key bindings', function() local result = help.help_lines() - assert.same({ + assert_same({ ' Triptych key bindings', ' ', ' add : a', diff --git a/tests/specs/ui_spec.lua b/tests/specs/ui_spec.lua index 33e398e..31d397e 100644 --- a/tests/specs/ui_spec.lua +++ b/tests/specs/ui_spec.lua @@ -1,8 +1,8 @@ -local assert = require 'luassert' local u = require 'tests.utils' local framework = require 'test_framework.test' local describe = framework.describe local test = framework.test_async +local assert_same = framework.assert_same local cwd = vim.fn.getcwd() local opening_dir = u.join_path(cwd, 'tests/test_playground/level_1/level_2/level_3') @@ -29,7 +29,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(is_open, true) + assert_same(is_open, true) -- This is kinda janky, but basically this test could be called from one of 2 places assert(current_line == 'run_specs.lua' or current_line == 'ui_spec.lua', 'got ' .. tostring(current_line)) end, @@ -43,7 +43,7 @@ describe('Triptych UI', { u.on_event('TriptychDidClose', function() done { assertions = function() - assert.same(vim.g.triptych_is_open, false) + assert_same(vim.g.triptych_is_open, false) end, } end) @@ -71,8 +71,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines, result.lines) - assert.same(expected_winbars, result.winbars) + assert_same(expected_lines, result.lines) + assert_same(expected_winbars, result.winbars) end, } end) @@ -99,8 +99,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines, result.lines) - assert.same(expected_winbars, result.winbars) + assert_same(expected_lines, result.lines) + assert_same(expected_winbars, result.winbars) end, } end) @@ -128,8 +128,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines, result.lines) - assert.same(expected_winbars, result.winbars) + assert_same(expected_lines, result.lines) + assert_same(expected_winbars, result.winbars) end, } end) @@ -145,7 +145,7 @@ describe('Triptych UI', { u.on_event('TriptychDidClose', function() done { assertions = function() - assert.same(vim.g.triptych_is_open, false) + assert_same(vim.g.triptych_is_open, false) end, cleanup = function() vim.api.nvim_set_current_buf(current_buf) @@ -173,7 +173,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_file_preview, state.lines.child) + assert_same(expected_file_preview, state.lines.child) end, } end) @@ -206,8 +206,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines.primary, state.lines.primary) - assert.same(expected_lines.child, state.lines.child) + assert_same(expected_lines.primary, state.lines.primary) + assert_same(expected_lines.child, state.lines.child) end, cleanup = function() vim.fn.delete(u.join_path(opening_dir, 'a_new_dir'), 'rf') @@ -243,7 +243,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_events, events) + assert_same(expected_events, events) end, cleanup = function() vim.fn.delete(u.join_path(opening_dir, 'a_new_dir'), 'rf') @@ -282,8 +282,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines.primary, state.lines.primary) - assert.same(expected_lines.child, state.lines.child) + assert_same(expected_lines.primary, state.lines.primary) + assert_same(expected_lines.child, state.lines.child) end, } end) @@ -318,8 +318,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines.child, state.lines.child) - assert.same(expected_lines.primary, state.lines.primary) + assert_same(expected_lines.child, state.lines.child) + assert_same(expected_lines.primary, state.lines.primary) end, cleanup = function() vim.fn.delete(u.join_path(cwd, 'tests/test_playground/level_1/level_2/level_4'), 'rf') @@ -357,7 +357,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_events, events) + assert_same(expected_events, events) end, cleanup = function() vim.fn.delete(u.join_path(cwd, 'tests/test_playground/level_1/level_2/level_4'), 'rf') @@ -393,7 +393,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_events, events) + assert_same(expected_events, events) end, cleanup = function() vim.fn.delete(u.join_path(opening_dir, 'level_3_file_1_copy1.md')) @@ -437,8 +437,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines.primary, state.lines.primary) - assert.same(expected_lines.child, state.lines.child) + assert_same(expected_lines.primary, state.lines.primary) + assert_same(expected_lines.child, state.lines.child) end, cleanup = function() vim.fn.delete(u.join_path(opening_dir, 'level_3_file_1_copy1.md')) @@ -476,7 +476,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines.primary, state.lines.primary) + assert_same(expected_lines.primary, state.lines.primary) end, cleanup = function() vim.fn.rename(u.join_path(opening_dir, 'renamed_dir'), u.join_path(opening_dir, 'level_4')) @@ -521,7 +521,7 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_events, events) + assert_same(expected_events, events) end, cleanup = function() vim.fn.rename(u.join_path(opening_dir, 'renamed_dir'), u.join_path(opening_dir, 'level_4')) @@ -566,9 +566,9 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_lines_without_hidden.primary, first_state.lines.primary) - assert.same(expected_lines_with_hidden.primary, second_state.lines.primary) - assert.same(expected_lines_without_hidden.primary, third_state.lines.primary) + assert_same(expected_lines_without_hidden.primary, first_state.lines.primary) + assert_same(expected_lines_with_hidden.primary, second_state.lines.primary) + assert_same(expected_lines_without_hidden.primary, third_state.lines.primary) end, cleanup = function() vim.fn.delete(git_ignored_file) @@ -600,8 +600,8 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_winbar_after_first_jump, winbar_after_first_jump) - assert.same(expected_winbar_after_second_jump, winbar_after_second_jump) + assert_same(expected_winbar_after_first_jump, winbar_after_first_jump) + assert_same(expected_winbar_after_second_jump, winbar_after_second_jump) end, } end) @@ -632,10 +632,10 @@ describe('Triptych UI', { close_triptych(function() done { assertions = function() - assert.same(expected_collapsed_lines.primary, state_collapsed.lines.primary) - assert.same(expected_collapsed_lines.child, state_collapsed.lines.child) - assert.same(expected_uncollapsed_lines.primary, state_uncollapsed.lines.primary) - assert.same(expected_uncollapsed_lines.child, state_uncollapsed.lines.child) + assert_same(expected_collapsed_lines.primary, state_collapsed.lines.primary) + assert_same(expected_collapsed_lines.child, state_collapsed.lines.child) + assert_same(expected_uncollapsed_lines.primary, state_uncollapsed.lines.primary) + assert_same(expected_uncollapsed_lines.child, state_uncollapsed.lines.child) end, cleanup = function() os.execute('rm -rf ' .. opening_dir .. '/a') @@ -678,8 +678,8 @@ describe('Triptych config', { close_triptych(function() done { assertions = function() - assert.same(expected_data, resulting_callback_params[1]) - assert.same('function', type(resulting_callback_params[2])) + assert_same(expected_data, resulting_callback_params[1]) + assert_same('function', type(resulting_callback_params[2])) end, } end) diff --git a/tests/specs/utils_spec.lua b/tests/specs/utils_spec.lua index 105f978..04f44cd 100644 --- a/tests/specs/utils_spec.lua +++ b/tests/specs/utils_spec.lua @@ -1,8 +1,8 @@ -local assert = require 'luassert' local u = require 'triptych.utils' local framework = require 'test_framework.test' local it = framework.test local describe = framework.describe +local assert_same = framework.assert_same describe('set', { it('returns a copy of the table with the specified value changed', function() @@ -11,7 +11,7 @@ describe('set', { bar = 2, } local result = u.set(tbl, 'foo', 3) - assert.same(3, result.foo) + assert_same(3, result.foo) end), }) @@ -37,7 +37,7 @@ describe('merge_tables', { }, } local result = u.merge_tables(a, b) - assert.same(expected, result) + assert_same(expected, result) end), it('merges tables - first one is empty', function() @@ -53,7 +53,7 @@ describe('merge_tables', { }, } local result = u.merge_tables(a, b) - assert.same(expected, result) + assert_same(expected, result) end), it('merges tables - second one empty', function() @@ -71,17 +71,17 @@ describe('merge_tables', { }, } local result = u.merge_tables(a, b) - assert.same(expected, result) + assert_same(expected, result) end), }) describe('round', { it('rounds to x decimal places', function() - assert.same(0.33, u.round(0.333, 2)) - assert.same(0.333, u.round(0.333, 3)) - assert.same(1.2, u.round(1.16, 1)) - assert.same(1.1, u.round(1.11, 1)) - assert.same(1, u.round(1.16, 0)) - assert.same(200.99, u.round(200.99, 3)) + assert_same(0.33, u.round(0.333, 2)) + assert_same(0.333, u.round(0.333, 3)) + assert_same(1.2, u.round(1.16, 1)) + assert_same(1.1, u.round(1.11, 1)) + assert_same(1, u.round(1.16, 0)) + assert_same(200.99, u.round(200.99, 3)) end), }) diff --git a/validate.sh b/validate.sh index c20ecde..5ff8b43 100755 --- a/validate.sh +++ b/validate.sh @@ -20,6 +20,7 @@ diagnostics_exit_code=$? if [ $diagnostics_exit_code -ne 0 ]; then echo "❌ 1 or more diagnostic problems found"; + exit 1; else echo "✅ Diagnostics passed"; fi @@ -30,6 +31,7 @@ tests_exit_code=$? if [ $tests_exit_code -ne 0 ]; then echo "❌ 1 or more tests failed"; + exit 1; else echo "✅ Tests passed"; fi