From 9d8b97373ac55fc095479092e227ffc2417a1921 Mon Sep 17 00:00:00 2001 From: goose Date: Sun, 8 Dec 2024 22:56:10 +0700 Subject: [PATCH] feat(actions): add action to toggle comment log statements --- README.md | 14 + doc/timber.nvim.txt | 14 + lua/timber/actions.lua | 8 + lua/timber/actions/clear.lua | 43 +-- lua/timber/actions/comment.lua | 49 ++++ lua/timber/actions/log_statements.lua | 50 ++++ tests/timber/actions/timber_actions_spec.lua | 272 +++++++++++++++---- tests/timber/helper.lua | 4 +- 8 files changed, 363 insertions(+), 91 deletions(-) create mode 100644 lua/timber/actions/comment.lua create mode 100644 lua/timber/actions/log_statements.lua diff --git a/README.md b/README.md index 7256f6c..471389f 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,20 @@ require("timber.actions").clear_log_statements({ global = true }) Be aware of [potential limitations](https://github.com/Goose97/timber.nvim/blob/a2faec8a7525d49a2e033ce54246cd50a4fb9021/doc/timber.nvim.txt#L245-L250). +### Comment log statements + +Comment/uncomment all log statements in the current buffer: + +```lua +require("timber.actions").toggle_comment_log_statements({ global = false }) +``` + +or from all buffers: + +```lua +require("timber.actions").toggle_comment_log_statements({ global = true }) +``` + ### Capture log results `timber.nvim` can monitor multiple sources and capture the log results. For example, a common use case is to capture the log results from a test runner or from a log file. diff --git a/doc/timber.nvim.txt b/doc/timber.nvim.txt index eecc444..10c8cb8 100644 --- a/doc/timber.nvim.txt +++ b/doc/timber.nvim.txt @@ -151,6 +151,7 @@ Users can invoke actions via the API. Action APIs are defined in the :h timber.actions.insert_batch_log :h timber.actions.add_log_targets_to_batch :h timber.actions.clear_log_statements + :h timber.actions.toggle_comment_log_statements actions.insert_log({opts}) *timber.actions.insert_log()* @@ -257,6 +258,19 @@ actions.clear_log_statements({opts}) *timber.actions.clear_log_statements()* or just the current one (false). Default: false + +actions.toggle_comment_log_statements({opts}) *timber.actions.toggle_comment_log_statements()* + + Comment/uncomment all log statements in the current buffer or all buffers. + + Parameters: ~ + {opts} (table) options to pass to the action + + Options: ~ + {global} (boolean) whether to toggle all buffers (true) + or just the current one (false). + Default: false + ============================================================================== 3. Watchers *timber.nvim-watchers* diff --git a/lua/timber/actions.lua b/lua/timber/actions.lua index 865b2f7..bae0e4c 100644 --- a/lua/timber/actions.lua +++ b/lua/timber/actions.lua @@ -743,6 +743,14 @@ function M.clear_log_statements(opts) require("timber.actions.clear").clear(opts) end +---@class Timber.Actions.CommentLogStatementsOptions +---@field global? boolean Whether to comment all buffers, or just the current buffer. Defaults to `false` +---@param opts Timber.Actions.CommentLogStatementsOptions? +function M.toggle_comment_log_statements(opts) + opts = vim.tbl_deep_extend("force", { global = false }, opts or {}) + require("timber.actions.comment").toggle_comment(opts) +end + function M.setup() treesitter.setup() end diff --git a/lua/timber/actions/clear.lua b/lua/timber/actions/clear.lua index 75c55f2..365da62 100644 --- a/lua/timber/actions/clear.lua +++ b/lua/timber/actions/clear.lua @@ -2,31 +2,15 @@ local M = {} local config = require("timber.config") local utils = require("timber.utils") +local log_statements = require("timber.actions.log_statements") -- Using grep to search all files globally local function clear_global(log_marker) - vim.cmd(string.format("silent! grep! %s", log_marker)) - - local qf_list = vim.fn.getqflist() local processed = {} - -- Sort quickfix entries by buffer and line number (in reverse) - table.sort(qf_list, function(a, b) - if a.bufnr == b.bufnr then - return a.lnum > b.lnum - end - return a.bufnr > b.bufnr - end) - - -- Delete lines (starting from bottom to preserve line numbers) - for _, item in ipairs(qf_list) do - local bufnr = item.bufnr - local lnum = item.lnum - - -- Delete the line + for bufnr, lnum in log_statements.iter_global(log_marker) do vim.api.nvim_buf_set_lines(bufnr, lnum - 1, lnum, false, {}) - -- Mark buffer as modified if not processed[bufnr] then processed[bufnr] = true end @@ -40,25 +24,6 @@ local function clear_global(log_marker) end end -local function clear_local(log_marker) - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) - local lines_to_delete = {} - - for i, line in ipairs(lines) do - -- Escape all non-word characters - if string.find(line, log_marker, 1, true) then - table.insert(lines_to_delete, i) - end - end - - -- Delete lines from bottom to top - -- We don't want the line number shifting - for i = #lines_to_delete, 1, -1 do - local line_num = lines_to_delete[i] - vim.api.nvim_buf_set_lines(0, line_num - 1, line_num, false, {}) - end -end - ---@param opts {global: boolean} function M.clear(opts) local log_marker = config.config.log_marker @@ -71,7 +36,9 @@ function M.clear(opts) if opts.global then clear_global(log_marker) else - clear_local(log_marker) + for linenr in log_statements.iter_local(log_marker) do + vim.api.nvim_buf_set_lines(0, linenr - 1, linenr, false, {}) + end end end diff --git a/lua/timber/actions/comment.lua b/lua/timber/actions/comment.lua new file mode 100644 index 0000000..0ace5a8 --- /dev/null +++ b/lua/timber/actions/comment.lua @@ -0,0 +1,49 @@ +local M = {} + +local config = require("timber.config") +local utils = require("timber.utils") +local log_statements = require("timber.actions.log_statements") + +-- Using grep to search all files globally +local function toggle_comment_global(log_marker) + local processed = {} + + for bufnr, lnum in log_statements.iter_global(log_marker) do + vim.api.nvim_buf_call(bufnr, function() + vim.api.nvim_win_set_cursor(0, { lnum, 0 }) + vim.cmd("normal gcc") + end) + + if not processed[bufnr] then + processed[bufnr] = true + end + end + + -- Save all modified buffers + for bufnr, _ in pairs(processed) do + vim.api.nvim_buf_call(bufnr, function() + vim.cmd("silent! write") + end) + end +end + +---@param opts {global: boolean} +function M.toggle_comment(opts) + local log_marker = config.config.log_marker + + if not log_marker or log_marker == "" then + utils.notify("config.log_marker is not configured", "warn") + return + end + + if opts.global then + toggle_comment_global(log_marker) + else + for linenr in log_statements.iter_local(log_marker) do + vim.api.nvim_win_set_cursor(0, { linenr, 0 }) + vim.cmd("normal gcc") + end + end +end + +return M diff --git a/lua/timber/actions/log_statements.lua b/lua/timber/actions/log_statements.lua new file mode 100644 index 0000000..f1ffd64 --- /dev/null +++ b/lua/timber/actions/log_statements.lua @@ -0,0 +1,50 @@ +local M = {} + +---@param log_marker string +function M.iter_global(log_marker) + vim.cmd(string.format("silent! grep! %s", log_marker)) + + local qf_list = vim.fn.getqflist() + + -- Sort quickfix entries by buffer and line number (in reverse) + table.sort(qf_list, function(a, b) + if a.bufnr == b.bufnr then + return a.lnum > b.lnum + end + return a.bufnr > b.bufnr + end) + + -- Iterator function + local i = 0 + return function() + i = i + 1 + local item = qf_list[i] + if item then + return item.bufnr, item.lnum + end + end +end + +---@param log_marker string +function M.iter_local(log_marker) + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local lines_to_delete = {} + + for i, line in ipairs(lines) do + if string.find(line, log_marker, 1, true) then + table.insert(lines_to_delete, i) + end + end + + -- Iterator function + local i = #lines_to_delete + return function() + if i > 0 then + local item = lines_to_delete[i] + i = i - 1 + return item + end + end +end + +return M diff --git a/tests/timber/actions/timber_actions_spec.lua b/tests/timber/actions/timber_actions_spec.lua index 4ac5333..f13069b 100644 --- a/tests/timber/actions/timber_actions_spec.lua +++ b/tests/timber/actions/timber_actions_spec.lua @@ -1296,18 +1296,13 @@ describe("timber.actions.clear_log_statements", function() input = [[ local fo|o = "foo" local bar = "bar" + print("foo", foo) ]], filetype = "lua", action = function() - vim.cmd("normal! vap") + vim.cmd("normal! vj") actions.insert_log({ position = "below" }) end, - expected = [[ - local foo = "foo" - print("🪵-TIMBER foo", foo) - local bar = "bar" - print("🪵-TIMBER bar", bar) - ]], }) local bufnr2 = helper.assert_scenario({ @@ -1317,19 +1312,12 @@ describe("timber.actions.clear_log_statements", function() ]], filetype = "lua", action = function() - vim.cmd("normal! vap") + vim.cmd("normal! vj") actions.insert_log({ position = "below" }) end, - expected = [[ - local foo = "foo" - print("🪵-TIMBER foo", foo) - local bar = "bar" - print("🪵-TIMBER bar", bar) - ]], }) vim.api.nvim_set_current_buf(bufnr1) - helper.wait(20) actions.clear_log_statements({ global = false }) helper.assert_buf_content( @@ -1337,6 +1325,7 @@ describe("timber.actions.clear_log_statements", function() [[ local foo = "foo" local bar = "bar" + print("foo", foo) ]] ) @@ -1378,48 +1367,30 @@ describe("timber.actions.clear_log_statements", function() input = [[ local fo|o = "foo" local bar = "bar" + print("foo", foo) ]], filetype = "lua", action = function() - vim.cmd("normal! vap") + vim.cmd("normal! vj") actions.insert_log({ position = "below" }) end, - expected = string.format( - [[ - local foo = "foo" - print("%s foo", foo) - local bar = "bar" - print("%s bar", bar) - ]], - log_marker, - log_marker - ), }) local bufnr2 = helper.assert_scenario({ input = [[ local fo|o = "foo" local bar = "bar" + print("foo", foo) ]], filetype = "lua", action = function() - vim.cmd("normal! vap") + vim.cmd("normal! vj") actions.insert_log({ position = "below" }) end, - expected = string.format( - [[ - local foo = "foo" - print("%s foo", foo) - local bar = "bar" - print("%s bar", bar) - ]], - log_marker, - log_marker - ), }) - write_buf_file(bufnr1, "test_sandbox/buffer1") - write_buf_file(bufnr2, "test_sandbox/buffer2") + write_buf_file(bufnr1, "test_sandbox/clear1") + write_buf_file(bufnr2, "test_sandbox/clear2") helper.wait(20) actions.clear_log_statements({ global = true }) @@ -1429,6 +1400,7 @@ describe("timber.actions.clear_log_statements", function() [[ local foo = "foo" local bar = "bar" + print("foo", foo) ]] ) @@ -1437,6 +1409,7 @@ describe("timber.actions.clear_log_statements", function() [[ local foo = "foo" local bar = "bar" + print("foo", foo) ]] ) end) @@ -1476,7 +1449,6 @@ describe("timber.actions.clear_log_statements", function() }) vim.api.nvim_set_current_buf(bufnr) - helper.wait(20) actions.clear_log_statements({ global = false }) helper.assert_buf_content( @@ -1494,51 +1466,249 @@ describe("timber.actions.clear_log_statements", function() notify_spy:clear() end) end) +end) - describe("given the log template DOES NOT contain %log_marker", function() +describe("timber.actions.toggle_comment_log_statements", function() + describe("given the global opts is false", function() before_each(function() timber.setup({ log_templates = { default = { - lua = [[print("%log_target", %log_target)]], + lua = [[print("%log_marker %log_target", %log_target)]], }, }, log_marker = "🪵-TIMBER", }) end) - it("DOES NOT clear any statements and notifies the user", function() - local bufnr = helper.assert_scenario({ + it("toggles comment all statements ONLY in the current buffer", function() + local bufnr1 = helper.assert_scenario({ input = [[ local fo|o = "foo" local bar = "bar" + print("foo", foo) ]], filetype = "lua", action = function() - vim.cmd("normal! vap") + vim.cmd("normal vj") actions.insert_log({ position = "below" }) end, - expected = [[ + }) + + local bufnr2 = helper.assert_scenario({ + input = [[ + local fo|o = "foo" + local bar = "bar" + ]], + filetype = "lua", + action = function() + vim.cmd("normal! vj") + actions.insert_log({ position = "below" }) + end, + }) + + vim.api.nvim_set_current_buf(bufnr1) + -- Comment + actions.toggle_comment_log_statements({ global = false }) + + helper.assert_buf_content( + bufnr1, + [[ local foo = "foo" + -- print("🪵-TIMBER foo", foo) + local bar = "bar" + -- print("🪵-TIMBER bar", bar) print("foo", foo) + ]] + ) + + helper.assert_buf_content( + bufnr2, + [[ + local foo = "foo" + print("🪵-TIMBER foo", foo) local bar = "bar" - print("bar", bar) + print("🪵-TIMBER bar", bar) + ]] + ) + + -- Uncomment + actions.toggle_comment_log_statements({ global = false }) + helper.assert_buf_content( + bufnr1, + [[ + local foo = "foo" + print("🪵-TIMBER foo", foo) + local bar = "bar" + print("🪵-TIMBER bar", bar) + print("foo", foo) + ]] + ) + end) + end) + + describe("given the global opts is true", function() + before_each(function() + vim.fn.system({ "rm", "-rf", "test_sandbox" }) + vim.fn.mkdir("test_sandbox") + local random = math.random(1000) + + timber.setup({ + log_templates = { + default = { + lua = [[print("%log_marker %log_target", %log_target)]], + }, + }, + log_marker = "🪵-" .. random, + }) + end) + + after_each(function() + vim.fn.system({ "rm", "-rf", "test_sandbox" }) + end) + + it("comments all statements in ALL buffers", function() + local log_marker = config.config.log_marker + local bufnr1 = helper.assert_scenario({ + input = [[ + local fo|o = "foo" + local bar = "bar" + print("foo", foo) + ]], + filetype = "lua", + action = function() + vim.cmd("normal! vj") + actions.insert_log({ position = "below" }) + end, + }) + + local bufnr2 = helper.assert_scenario({ + input = [[ + local fo|o = "foo" + local bar = "bar" + print("foo", foo) ]], + filetype = "lua", + action = function() + vim.cmd("normal! vj") + actions.insert_log({ position = "below" }) + end, + }) + + write_buf_file(bufnr1, "test_sandbox/comment1") + write_buf_file(bufnr2, "test_sandbox/comment2") + + -- Comment + actions.toggle_comment_log_statements({ global = true }) + + helper.assert_buf_content( + bufnr1, + string.format( + [[ + local foo = "foo" + -- print("%s foo", foo) + local bar = "bar" + -- print("%s bar", bar) + print("foo", foo) + ]], + log_marker, + log_marker + ) + ) + + helper.assert_buf_content( + bufnr2, + string.format( + [[ + local foo = "foo" + -- print("%s foo", foo) + local bar = "bar" + -- print("%s bar", bar) + print("foo", foo) + ]], + log_marker, + log_marker + ) + ) + + -- Uncomment + actions.toggle_comment_log_statements({ global = true }) + + helper.assert_buf_content( + bufnr1, + string.format( + [[ + local foo = "foo" + print("%s foo", foo) + local bar = "bar" + print("%s bar", bar) + print("foo", foo) + ]], + log_marker, + log_marker + ) + ) + + helper.assert_buf_content( + bufnr2, + string.format( + [[ + local foo = "foo" + print("%s foo", foo) + local bar = "bar" + print("%s bar", bar) + print("foo", foo) + ]], + log_marker, + log_marker + ) + ) + end) + end) + + describe("given the config.log_marker is not set or empty", function() + before_each(function() + timber.setup({ + log_templates = { + default = { + lua = [[print("%log_marker %log_target", %log_target)]], + }, + }, + log_marker = "", + }) + end) + + it("DOES NOT comment any statements and notifies the user", function() + local notify_spy = spy.on(utils, "notify") + + local bufnr = helper.assert_scenario({ + input = [[ + local fo|o = "foo" + local bar = "bar" + ]], + filetype = "lua", + action = function() + vim.cmd("normal! vap") + actions.insert_log({ position = "below" }) + end, }) vim.api.nvim_set_current_buf(bufnr) - helper.wait(20) - actions.clear_log_statements({ global = false }) + actions.toggle_comment_log_statements({ global = false }) helper.assert_buf_content( bufnr, [[ local foo = "foo" - print("foo", foo) + print(" foo", foo) local bar = "bar" - print("bar", bar) + print(" bar", bar) ]] ) + + assert.spy(notify_spy).was_called(1) + assert.spy(notify_spy).was_called_with("config.log_marker is not configured", "warn") + notify_spy:clear() end) end) end) diff --git a/tests/timber/helper.lua b/tests/timber/helper.lua index 83f5694..e78f9cd 100644 --- a/tests/timber/helper.lua +++ b/tests/timber/helper.lua @@ -66,7 +66,7 @@ end ---@field input_cursor? string|boolean ---@field filetype string ---@field action function? ----@field expected string | function +---@field expected? string | function ---Given an input, execute a callback, and assert the expected output. ---The input supports specifying the cursor position with a pipe character. @@ -89,7 +89,7 @@ function M.assert_scenario(scenario) local expected = scenario.expected if type(expected) == "function" then expected() - else + elseif type(expected) == "string" then local expected_lines, cursor1 = parse_input(expected, scenario.input_cursor) if cursor1 then