From bfe374ba423b36df2c0cefc907432da4784457df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20NICOLAS?= Date: Sun, 15 Dec 2024 20:18:29 +0100 Subject: [PATCH 01/11] Highlight farthest coinciding ancestor Up until now, the node while going Up and Down would not be highlighted consistently with those you travel to while browsing Left and Right. In simple words, you may only be "selecting" (understand: highlighting) a keyword like `if` when you'd expect to "select" the entire if-block. This commit remedies that issue by selecting the farthest ancestor of the candidate node that shares its starting coordinates. --- lua/treewalker/strategies.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lua/treewalker/strategies.lua b/lua/treewalker/strategies.lua index 0a3956a..3d692fd 100644 --- a/lua/treewalker/strategies.lua +++ b/lua/treewalker/strategies.lua @@ -20,6 +20,16 @@ local function get_node_from_neighboring_line(current_row, dir) local candidate_line = lines.get_line(candidate_row) local candidate_col = lines.get_start_col(candidate_line) local candidate = nodes.get_at_rowcol(candidate_row, candidate_col) + + -- Get farthest ancestor _which starts at the same coordinates as the candidate_ + if candidate then + local next = candidate:parent() + while next and nodes.have_same_start(candidate, next) do + if nodes.is_jump_target(next) then candidate = next end + next = next:parent() + end + end + return candidate, candidate_row, candidate_line end From b6efde27d668a9a504ee7a494cc7589205035106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20NICOLAS?= Date: Sun, 15 Dec 2024 20:38:14 +0100 Subject: [PATCH 02/11] Highlight node as expected on all jumps This has the nice side-effect of actually only ever computing the ideal node to highlight when the user actually wants their jumps to be highlighted. --- lua/treewalker/ops.lua | 6 ++++++ lua/treewalker/strategies.lua | 10 ---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lua/treewalker/ops.lua b/lua/treewalker/ops.lua index 167c84d..8fe7b64 100644 --- a/lua/treewalker/ops.lua +++ b/lua/treewalker/ops.lua @@ -63,6 +63,12 @@ function M.jump(row, node) vim.api.nvim_win_set_cursor(0, { row, 0 }) vim.cmd('normal! ^') if require("treewalker").opts.highlight then + -- Get farthest ancestor (or self) at the same starting coordinates + local parent = node:parent() + while parent and nodes.have_same_start(node, parent) do + if nodes.is_jump_target(parent) then node = parent end + parent = parent:parent() + end M.highlight(nodes.range(node)) end end diff --git a/lua/treewalker/strategies.lua b/lua/treewalker/strategies.lua index 3d692fd..0a3956a 100644 --- a/lua/treewalker/strategies.lua +++ b/lua/treewalker/strategies.lua @@ -20,16 +20,6 @@ local function get_node_from_neighboring_line(current_row, dir) local candidate_line = lines.get_line(candidate_row) local candidate_col = lines.get_start_col(candidate_line) local candidate = nodes.get_at_rowcol(candidate_row, candidate_col) - - -- Get farthest ancestor _which starts at the same coordinates as the candidate_ - if candidate then - local next = candidate:parent() - while next and nodes.have_same_start(candidate, next) do - if nodes.is_jump_target(next) then candidate = next end - next = next:parent() - end - end - return candidate, candidate_row, candidate_line end From 39ccff67ada7cbcebd7015d4ac9bf8071cc8627c Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 14:42:53 -0800 Subject: [PATCH 03/11] Create config defaults Insodoing, flips the default highlight behavior from false to true. I think this is fine b/c the readme has highlight explicit in the prescribed installation, so people should have that in there. Hopefully if they don't, this isn't too jarring. --- README.md | 2 +- lua/treewalker/init.lua | 5 ++++- plugin/init.lua | 3 +-- tests/treewalker/acceptance_spec.lua | 7 ++++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cd4d2bb..52f5958 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Moving slowly, showing each command { "aaronik/treewalker.nvim", opts = { - highlight = true -- default is false + highlight = true -- briefly highlight the node after jumping to it } } ``` diff --git a/lua/treewalker/init.lua b/lua/treewalker/init.lua index 286b396..437503d 100644 --- a/lua/treewalker/init.lua +++ b/lua/treewalker/init.lua @@ -8,8 +8,11 @@ local Treewalker = {} ---@alias Opts { highlight: boolean } +-- Default setup() options ---@type Opts -Treewalker.opts = {} +Treewalker.opts = { + highlight = true +} ---@param opts Opts | nil function Treewalker.setup(opts) diff --git a/plugin/init.lua b/plugin/init.lua index fc1a3f5..388e7ef 100644 --- a/plugin/init.lua +++ b/plugin/init.lua @@ -1,6 +1,5 @@ -local util = require "treewalker.util" - local function tw() + -- local util = require "treewalker.util" -- return util.R('treewalker') return require('treewalker') end diff --git a/tests/treewalker/acceptance_spec.lua b/tests/treewalker/acceptance_spec.lua index e53310a..6dfe34a 100644 --- a/tests/treewalker/acceptance_spec.lua +++ b/tests/treewalker/acceptance_spec.lua @@ -80,15 +80,15 @@ describe("Treewalker", function() it("respects highlight config option", function() local highlight_stub = stub(ops, "highlight") - - treewalker.setup() + treewalker.setup() -- highlight defaults to true, doesn't blow up with empty setup vim.fn.cursor(23, 5) treewalker.move_out() treewalker.move_down() treewalker.move_up() treewalker.move_in() - assert.equal(0, #highlight_stub.calls) + assert.equal(4, #highlight_stub.calls) + highlight_stub = stub(ops, "highlight") treewalker.setup({ highlight = false }) vim.fn.cursor(23, 5) treewalker.move_out() @@ -97,6 +97,7 @@ describe("Treewalker", function() treewalker.move_in() assert.equal(0, #highlight_stub.calls) + highlight_stub = stub(ops, "highlight") treewalker.setup({ highlight = true }) vim.fn.cursor(23, 5) treewalker.move_out() From f54d28bfa8db0ea0b2c8720c825a582fd5922872 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 14:48:26 -0800 Subject: [PATCH 04/11] Add ancestor related helper to nodes This is goign to be important when it comes to correctly identifying the current node, which is a Big Problem. This has an impact on both highlighting and on node swapping. --- lua/treewalker/nodes.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lua/treewalker/nodes.lua b/lua/treewalker/nodes.lua index 7fdc6db..eea05c2 100644 --- a/lua/treewalker/nodes.lua +++ b/lua/treewalker/nodes.lua @@ -102,6 +102,27 @@ function M.get_descendants(node) return descendants end +---@param node TSNode +---@return TSNode +function M.get_farthest_ancestor_with_same_srow(node) + local node_row = node:range() + local farthest_ancestor = node + local iter_row = node:range() + local iter = node:parent() + + + while iter do + iter_row = iter:range() + if iter_row ~= node_row then + break + end + farthest_ancestor = iter + iter = iter:parent() + end + + return farthest_ancestor +end + --- Take a list of nodes and unique them based on line start ---@param nodes TSNode[] ---@return TSNode[] From c4a2312b62063f5ebb873650c80aca171c7ce10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20NICOLAS?= Date: Sun, 15 Dec 2024 23:40:48 +0100 Subject: [PATCH 05/11] Avoid highlighting 'block'-like nodes When jumping to the first statement of a code block, in some languages, the attempt at highlighting the outermost ancestor that shares the same starting coordinates would erroneously highlight the entire code block. The same may happen for the first statement of a file, which would previously highlight the entire root node, and consequently: the entire file. --- lua/treewalker/nodes.lua | 11 +++++++++++ lua/treewalker/ops.lua | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lua/treewalker/nodes.lua b/lua/treewalker/nodes.lua index 7fdc6db..12333be 100644 --- a/lua/treewalker/nodes.lua +++ b/lua/treewalker/nodes.lua @@ -15,6 +15,13 @@ local TARGET_DESCENDANT_TYPES = { "do_block", -- rb } +local HIGHLIGHT_BLACKLIST_TYPES = { + "body_statement", -- lua, rb + "block", -- lua + "statement_block", -- lua + "program", -- rb +} + local M = {} ---@param node TSNode @@ -33,6 +40,10 @@ function M.is_descendant_jump_target(node) return util.contains(TARGET_DESCENDANT_TYPES, node:type()) end +function M.is_highlight_target(node) + return util.contains(HIGHLIGHT_BLACKLIST_TYPES, node:type()) +end + ---Do the nodes have the same starting point ---@param node1 TSNode ---@param node2 TSNode diff --git a/lua/treewalker/ops.lua b/lua/treewalker/ops.lua index 8fe7b64..488e397 100644 --- a/lua/treewalker/ops.lua +++ b/lua/treewalker/ops.lua @@ -66,7 +66,7 @@ function M.jump(row, node) -- Get farthest ancestor (or self) at the same starting coordinates local parent = node:parent() while parent and nodes.have_same_start(node, parent) do - if nodes.is_jump_target(parent) then node = parent end + if nodes.is_highlight_target(parent) then node = parent end parent = parent:parent() end M.highlight(nodes.range(node)) From 3af10f97b8ca7a0d41e7ee0052939db0fd111b36 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 14:57:08 -0800 Subject: [PATCH 06/11] Add github action for running tests --- .github/workflows/test.yml | 39 ++++++++++++++++++++++++++++++++++ Makefile | 6 ++---- lua/treewalker/util.lua | 6 ------ tests/minimal_init.lua | 7 ------ tests/treewalker/util_spec.lua | 13 ------------ 5 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ad95f48 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Run Tests + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ main ] + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Install Make Dependencies + run: | + sudo apt-get update + sudo apt-get install libtool-bin autoconf automake cmake g++ pkg-config unzip gettext curl -y + + - name: Install Neovim + run: | + sudo snap install nvim --classic + + - name: Install Plenary + run: | + git clone https://github.com/nvim-lua/plenary.nvim.git + mkdir -p .local/share/nvim/lazy/ + mv plenary.nvim .local/share/nvim/lazy/ + + - name: Run Tests + env: + XDG_CONFIG_HOME: ${{ github.workspace }}/.config + XDG_DATA_HOME: ${{ github.workspace }}/.local/share + XDG_STATE_HOME: ${{ github.workspace }}/.local/state + XDG_CACHE_HOME: ${{ github.workspace }}/.cache + run: make test diff --git a/Makefile b/Makefile index fe134cd..3d8fa51 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,6 @@ TESTS_DIR=tests .PHONY: test -# TODO I want test to contain a lua-language-server pass - test_nvim: @nvim \ --headless \ @@ -13,7 +11,7 @@ test_nvim: -c "PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }" test: - -$(MAKE) test_nvim || exit 1 + $(MAKE) test_nvim test-watch: - nodemon -e lua -x "$(MAKE) test" + nodemon -e lua -x "$(MAKE) test || exit 1" diff --git a/lua/treewalker/util.lua b/lua/treewalker/util.lua index 5a9b25f..75110e9 100644 --- a/lua/treewalker/util.lua +++ b/lua/treewalker/util.lua @@ -98,12 +98,6 @@ M.guid = function() end) end ----@param env_key string ----@return boolean -M.has_env_var = function(env_key) - return type(os.getenv(env_key)) ~= type(nil) -end - ---reverse an array table ---@param t table M.reverse = function (t) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 1117c70..c8be46f 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -2,13 +2,6 @@ local lazypath = vim.fn.stdpath("data") .. "/lazy" vim.notify = print vim.opt.rtp:append(".") vim.opt.rtp:append(lazypath .. "/plenary.nvim") -vim.opt.rtp:append(lazypath .. "/nui.nvim") -vim.opt.rtp:append(lazypath .. "/telescope.nvim") --- vim.opt.rtp:append(lazypath .. "/nvim-nio") - --- -- Get all our normal plugins into the test env --- local suite = os.getenv("SUITE") --- vim.opt.rtp:append(suite .. "nvim") vim.opt.swapfile = false diff --git a/tests/treewalker/util_spec.lua b/tests/treewalker/util_spec.lua index 0e569ad..163941b 100644 --- a/tests/treewalker/util_spec.lua +++ b/tests/treewalker/util_spec.lua @@ -91,19 +91,6 @@ describe("util", function() end) end) - describe("ensure_env_var", function() - it("returns true", function() - -- always set - local res = util.has_env_var("SHELL") - assert.is_true(res) - end) - - it("returns false", function() - local res = util.has_env_var("IM_SUPER_SURE_THIS_ENV_VAR_WONT_BE_SET_FR_FR") - assert.is_false(res) - end) - end) - describe("reverse", function() it("reverses an array table", function() local t = { 1, 2, 3, 4, 5 } From dbd4f6bab44043685baa9e227b26d90ebb6bcb49 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 20:22:59 -0800 Subject: [PATCH 07/11] Remove vestigial code So I got excited and missed some stuff, sue me ;) --- lua/treewalker/nodes.lua | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lua/treewalker/nodes.lua b/lua/treewalker/nodes.lua index eea05c2..7652065 100644 --- a/lua/treewalker/nodes.lua +++ b/lua/treewalker/nodes.lua @@ -1,27 +1,16 @@ local util = require "treewalker.util" local lines= require "treewalker.lines" -local NON_TARGET_NODE_MATCHERS = { - -- "chunk", -- lua +local TARGET_TYPE_BLACKLIST = { "^.*comment.*$", } -local TARGET_DESCENDANT_TYPES = { - "body_statement", -- lua, rb - "block", -- lua - "statement_block", -- lua - - -- "then", -- helps rb, hurts lua - "do_block", -- rb -} - local M = {} ---@param node TSNode ---@return boolean function M.is_jump_target(node) - for _, matcher in ipairs(NON_TARGET_NODE_MATCHERS) do - -- If it's a banned type + for _, matcher in ipairs(TARGET_TYPE_BLACKLIST) do if node:type():match(matcher) then return false end @@ -29,10 +18,6 @@ function M.is_jump_target(node) return true end -function M.is_descendant_jump_target(node) - return util.contains(TARGET_DESCENDANT_TYPES, node:type()) -end - ---Do the nodes have the same starting point ---@param node1 TSNode ---@param node2 TSNode From 079f9f24f28df71de739e6591af0f814ea087026 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 21:29:47 -0800 Subject: [PATCH 08/11] Iterate on highlighting scheme Get some polish on the highlighting -- abstract where needed, blend, meld, mix, and incorporate other parts of the codebase. Major change here is using regex matchers on highlight target black list. Works on my machine, that is to say, on the languages I have here so far. Next step is to test more filetypes - got errors about not having the right TS parsers installed for the languages in the fixtures. So either gotta bootstrap more parsers into the minimal test init, or just go with what's built in to nvim, which I think includes C. --- lua/treewalker/nodes.lua | 106 ++++++++++++++++++++------------------- lua/treewalker/ops.lua | 7 +-- 2 files changed, 55 insertions(+), 58 deletions(-) diff --git a/lua/treewalker/nodes.lua b/lua/treewalker/nodes.lua index 688d831..512493c 100644 --- a/lua/treewalker/nodes.lua +++ b/lua/treewalker/nodes.lua @@ -1,32 +1,43 @@ local util = require "treewalker.util" -local lines= require "treewalker.lines" +local lines = require "treewalker.lines" -local TARGET_TYPE_BLACKLIST = { - "^.*comment.*$", +-- These are regexes but just happen to be real simple so far +local TARGET_BLACKLIST_TYPE_MATCHERS = { + "comment", } -local HIGHLIGHT_BLACKLIST_TYPES = { - "body_statement", -- lua, rb - "block", -- lua - "statement_block", -- lua - "program", -- rb +local HIGHLIGHT_BLACKLIST_TYPE_MATCHERS = { + "chunk", + "body", + "block", + "program", } + local M = {} ---@param node TSNode +---@param matchers string[] ---@return boolean -function M.is_jump_target(node) - for _, matcher in ipairs(TARGET_TYPE_BLACKLIST) do +local function is_matched_in(node, matchers) + for _, matcher in ipairs(matchers) do if node:type():match(matcher) then - return false + return true end end - return true + return false +end + +---@param node TSNode +---@return boolean +function M.is_jump_target(node) + return not is_matched_in(node, TARGET_BLACKLIST_TYPE_MATCHERS) end +---@param node TSNode +---@return boolean function M.is_highlight_target(node) - return util.contains(HIGHLIGHT_BLACKLIST_TYPES, node:type()) + return not is_matched_in(node, HIGHLIGHT_BLACKLIST_TYPE_MATCHERS) end ---Do the nodes have the same starting point @@ -79,62 +90,53 @@ end ---@param node TSNode ---@return TSNode[] function M.get_descendants(node) - local descendants = {} - - -- Helper function to recursively collect descendants - local function collect_descendants(current_node) - local child_count = current_node:child_count() - for i = 0, child_count - 1 do - local child = current_node:child(i) - table.insert(descendants, child) - -- Recursively collect descendants of the child - collect_descendants(child) - end + local descendants = {} + + -- Helper function to recursively collect descendants + local function collect_descendants(current_node) + local child_count = current_node:child_count() + for i = 0, child_count - 1 do + local child = current_node:child(i) + table.insert(descendants, child) + -- Recursively collect descendants of the child + collect_descendants(child) end + end - -- Start the recursive collection with the given node - collect_descendants(node) + -- Start the recursive collection with the given node + collect_descendants(node) - return descendants + return descendants end +-- Get farthest ancestor (or self) at the same starting coordinates ---@param node TSNode ---@return TSNode function M.get_farthest_ancestor_with_same_srow(node) - local node_row = node:range() - local farthest_ancestor = node - local iter_row = node:range() - local iter = node:parent() - - - while iter do - iter_row = iter:range() - if iter_row ~= node_row then - break - end - farthest_ancestor = iter - iter = iter:parent() + local parent = node:parent() + while parent and M.have_same_start(node, parent) do + if M.is_highlight_target(parent) then node = parent end + parent = parent:parent() end - - return farthest_ancestor + return node end --- Take a list of nodes and unique them based on line start ---@param nodes TSNode[] ---@return TSNode[] function M.unique_per_line(nodes) - local unique_nodes = {} - local seen_lines = {} - - for _, node in ipairs(nodes) do - local line = node:start() -- Assuming node:start() returns the line number of the node - if not seen_lines[line] then - table.insert(unique_nodes, node) - seen_lines[line] = true - end + local unique_nodes = {} + local seen_lines = {} + + for _, node in ipairs(nodes) do + local line = node:start() -- Assuming node:start() returns the line number of the node + if not seen_lines[line] then + table.insert(unique_nodes, node) + seen_lines[line] = true end + end - return unique_nodes + return unique_nodes end -- Easy conversion to table diff --git a/lua/treewalker/ops.lua b/lua/treewalker/ops.lua index 488e397..7d25706 100644 --- a/lua/treewalker/ops.lua +++ b/lua/treewalker/ops.lua @@ -63,12 +63,7 @@ function M.jump(row, node) vim.api.nvim_win_set_cursor(0, { row, 0 }) vim.cmd('normal! ^') if require("treewalker").opts.highlight then - -- Get farthest ancestor (or self) at the same starting coordinates - local parent = node:parent() - while parent and nodes.have_same_start(node, parent) do - if nodes.is_highlight_target(parent) then node = parent end - parent = parent:parent() - end + node = nodes.get_farthest_ancestor_with_same_srow(node) M.highlight(nodes.range(node)) end end From 60b6ff357ff01793e155910cac30dc0a45466418 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 22:37:27 -0800 Subject: [PATCH 09/11] Max out highlight work * Add more fixtures / improve existing ones * These are really helpful for testing out the plugin while it's being written * Use row checks rather than starts for coincident nodes * Add more blacklist matchers * This here is the bummer. But since it's just the highlighting, with these the plugin is behaving more correctly than without. So maybe some day these can be sunset, but for now, they live on. --- lua/treewalker/nodes.lua | 27 +- lua/treewalker/ops.lua | 9 +- tests/fixtures/c.c | 57 ++++ tests/fixtures/haskell.hs | 51 +++- tests/fixtures/lua.lua | 3 + tests/fixtures/python.py | 24 ++ tests/fixtures/rust.rs | 255 +++--------------- tests/fixtures/scheme.scm | 55 +++- tests/treewalker/highlight_spec.lua | 88 ++++++ ...{acceptance_spec.lua => movement_spec.lua} | 35 +-- 10 files changed, 326 insertions(+), 278 deletions(-) create mode 100644 tests/fixtures/c.c create mode 100644 tests/treewalker/highlight_spec.lua rename tests/treewalker/{acceptance_spec.lua => movement_spec.lua} (74%) diff --git a/lua/treewalker/nodes.lua b/lua/treewalker/nodes.lua index 512493c..a0476b8 100644 --- a/lua/treewalker/nodes.lua +++ b/lua/treewalker/nodes.lua @@ -7,10 +7,14 @@ local TARGET_BLACKLIST_TYPE_MATCHERS = { } local HIGHLIGHT_BLACKLIST_TYPE_MATCHERS = { - "chunk", - "body", - "block", - "program", + "module", -- python + "chunk", -- lua + "body", -- ruby + "block", -- ruby + "program", -- ruby + "haskell", -- guess which language starts their module tree with this node + "translation_unit", -- c module + "source_file", -- rust } @@ -52,6 +56,14 @@ function M.have_same_start(node1, node2) scol1 == scol2 end +---Do the nodes have the same starting row +---@param node1 TSNode +---@param node2 TSNode +---@return boolean +function M.have_same_row(node1, node2) + return M.get_row(node1) == M.get_row(node2) +end + ---Do the nodes have the same level of indentation ---@param node1 TSNode ---@param node2 TSNode @@ -109,12 +121,13 @@ function M.get_descendants(node) return descendants end --- Get farthest ancestor (or self) at the same starting coordinates +-- Get farthest ancestor (or self) at the same starting row ---@param node TSNode ---@return TSNode -function M.get_farthest_ancestor_with_same_srow(node) +function M.get_highest_coincident(node) local parent = node:parent() - while parent and M.have_same_start(node, parent) do + -- prefer row over start on account of lisps / S-expressions, which start with (identifier, ..) + while parent and M.have_same_row(node, parent) do if M.is_highlight_target(parent) then node = parent end parent = parent:parent() end diff --git a/lua/treewalker/ops.lua b/lua/treewalker/ops.lua index 7d25706..abd5fe2 100644 --- a/lua/treewalker/ops.lua +++ b/lua/treewalker/ops.lua @@ -16,13 +16,6 @@ end local M = {} ----set cursor without throwing error ----@param row integer ----@param col integer -function M.safe_set_cursor(row, col) - pcall(vim.api.nvim_win_set_cursor, 0, { row, col }) -- catch any errors in nvim_win_set_cursor -end - ---Flash a highlight over the given range ---@param range Range4 function M.highlight(range) @@ -63,7 +56,7 @@ function M.jump(row, node) vim.api.nvim_win_set_cursor(0, { row, 0 }) vim.cmd('normal! ^') if require("treewalker").opts.highlight then - node = nodes.get_farthest_ancestor_with_same_srow(node) + node = nodes.get_highest_coincident(node) M.highlight(nodes.range(node)) end end diff --git a/tests/fixtures/c.c b/tests/fixtures/c.c new file mode 100644 index 0000000..ee05399 --- /dev/null +++ b/tests/fixtures/c.c @@ -0,0 +1,57 @@ +#include +#include + +// Structure to represent an account +typedef struct { + int accountNumber; + float balance; +} Account; + +// Function to create a new account +Account* createAccount(int accountNumber, float initialBalance) { + Account* newAccount = (Account*)malloc(sizeof(Account)); + if (!newAccount) { + printf("Memory error\n"); + return NULL; + } + newAccount->accountNumber = accountNumber; + newAccount->balance = initialBalance; + return newAccount; +} + +// Function to deposit money into an account +void deposit(Account* account, float amount) { + if (amount > 0.0f) { + account->balance += amount; + printf("Deposited $%.2f into account %d\n", amount, account->accountNumber); + } else { + printf("Invalid deposit amount: $%.2f\n", amount); + } +} + +// Function to withdraw money from an account +void withdraw(Account* account, float amount) { + if (amount > 0.0f && amount <= account->balance) { + account->balance -= amount; + printf("Withdrawn $%.2f from account %d\n", amount, account->accountNumber); + } else { + printf("Invalid withdrawal amount: $%.2f\n", amount); + } +} + +// Function to display account information +void printAccountInfo(Account* account) { + printf("Account Number: %d\nBalance: $%.2f\n", account->accountNumber, account->balance); +} + +int main() { + Account* account = createAccount(12345, 1000.00f); + + deposit(account, 500.00f); + withdraw(account, 200.00f); + printAccountInfo(account); + + free(account); + return 0; +} + diff --git a/tests/fixtures/haskell.hs b/tests/fixtures/haskell.hs index 95681cc..1fa3ced 100644 --- a/tests/fixtures/haskell.hs +++ b/tests/fixtures/haskell.hs @@ -1,9 +1,46 @@ -import Data.List +-- Import the necessary modules +import Data.List (sort) +import Control.Monad (replicateM) -permutations :: [a] -> [[a]] -permutations [] = [[]] -permutations xs = do - x <- xs - xsRest <- permutations $ filter (/=x) xs - return $ map (x:) xsRest +-- Define a function to print out all even numbers in a list +printEvens :: [Int] -> IO () +printEvens [] = return () +printEvens (x : xs) + | x `mod` 2 == 0 = putStrLn (show x) >> printEvens xs + | otherwise = printEvens xs +-- Define a function to calculate the sum of all numbers in a list +sumNumbers :: [Int] -> Int +sumNumbers [] = 0 +sumNumbers (x : xs) = x + sumNumbers xs + +-- Define a function to generate a random list of numbers +randomList :: IO [Int] +randomList = do + n <- getLine + let n' = read n :: Int + replicateM n (getRandomR (-100, 100)) >>= return . sort + +-- Define a function to calculate the median of a list of numbers +median :: [Double] -> Double +median xs = median' (sort xs) + where + median' [] = error "Empty list" + median' [_] = error "List contains single element" + median' xs + | odd len = fromIntegral $ xs !! (len `div` 2) + | otherwise = mean + where + len = length xs + mean = (sum xs) / fromIntegral len + +-- Main function to run the program +main :: IO () +main = do + printEvens [1, 3, 5, 7, 9] + print $ sumNumbers [-2, -4, 0, 10] + randomList >>= mapM_ putStrLn . map show + let xs = [-3.0, -1.0, 0.0, 1.0, 3.0] + ys = [5.5, 6.6] + print $ median xs + print $ sumNumbers ys diff --git a/tests/fixtures/lua.lua b/tests/fixtures/lua.lua index 034bdc9..02a2ce5 100644 --- a/tests/fixtures/lua.lua +++ b/tests/fixtures/lua.lua @@ -183,3 +183,6 @@ function M.get_node() end return M + +-- This is a lua fixture. I thought I was being smart when I got it +-- from this plugin. But actually I was being dumb, and this is very confusing, l0lz diff --git a/tests/fixtures/python.py b/tests/fixtures/python.py index b865738..72d6738 100644 --- a/tests/fixtures/python.py +++ b/tests/fixtures/python.py @@ -1,3 +1,27 @@ +class Person: + def __init__(self, name): + self.name = name + + def greet(self): + print(f"Hello, my name is {self.name}!") + +class Book: + def __init__(self, title, author): + self.title = title + self.author = author + + def describe(self): + print(f"{self.title} by {self.author}") + +class Car: + def __init__(self, make, model, year): + self.make = make + self.model = model + self.year = year + + def display_info(self): + print(f"Make: {self.make}, Model: {self.model}, Year: {self.year}") + def main(): """ This function demonstrates a nested structure. diff --git a/tests/fixtures/rust.rs b/tests/fixtures/rust.rs index 5121029..e9ec49d 100644 --- a/tests/fixtures/rust.rs +++ b/tests/fixtures/rust.rs @@ -1,244 +1,65 @@ -use std::collections::HashMap; - use rand::{thread_rng, Rng}; use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter}; use serde::{Serialize, Deserialize}; -// Map of neuron id -> .. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NeuralNet { - pub input_neurons: HashMap, - pub inner_neurons: HashMap, - pub output_neurons: HashMap, - - /// A mapping of neuron id -> neuron type, useful if you have a neuron id and want to find out - /// what type of neuron it is. - neuron_type_map: HashMap, -} - -/// The brain of a lifeform. Has three neuron types: Input, Inner, and Output. Input has -/// information from the world, Inner creates a recursive structure, and Output creates actions the -/// lifeform takes on the world. -impl NeuralNet { - pub fn new(num_inner_neurons: usize) -> Self { - let mut input_neurons = HashMap::new(); - let mut output_neurons = HashMap::new(); - let mut inner_neurons = HashMap::new(); - let mut neuron_type_map = HashMap::new(); - - // -- Generate Neurons - - for (idx, neuron_member) in InputNeuronType::iter().enumerate() { - // Assuming there'll never be more than 100 input neuron types, we'll do this - // to assure a different id from the other neurons - let id = idx + 100; - let neuron = InputNeuron { id, value: 0.0 }; - input_neurons.insert(id, (neuron_member, neuron)); - neuron_type_map.insert(id, NeuronType::InputNeuron); - } - - // Note: If the 200 here changes, it needs to be changed in add_inner_neuron as well - for idx in 0..num_inner_neurons { - let id = idx + 200; - let neuron = InnerNeuron { id }; - inner_neurons.insert(id, neuron); - neuron_type_map.insert(id, NeuronType::InnerNeuron); - } - - for (idx, neuron_member) in OutputNeuronType::iter().enumerate() { - let id = idx + 300; - let neuron = OutputNeuron { id }; - output_neurons.insert(id, (neuron_member, neuron)); - neuron_type_map.insert(id, NeuronType::OutputNeuron); - } - - // -- Generate Neuron Ids - - Self { - input_neurons, - output_neurons, - inner_neurons, - neuron_type_map, - } - } - - /// Returns a neuron id randomly chosen from input neurons unioned with inner neurons. - /// This is all the places where a gene can start from. - /// Takes an optional "not" value, which, if supplied, will prevent this from returning - /// that value. - pub fn random_from_neuron(&self, not_id: Option) -> usize { - let num_neurons = self.input_neurons.len() + self.inner_neurons.len(); - let idx = thread_rng().gen_range(0..num_neurons); - - let id: usize; - - if idx < self.input_neurons.len() { - let ids = &self.input_neurons.keys().map(|k| *k).collect(); - id = get_id_not_id(ids, idx, not_id); - } else { - let ids = &self.inner_neurons.keys().map(|k| *k).collect(); - let index = idx - self.input_neurons.len(); - id = get_id_not_id(ids, index, not_id); - } - - id - } - - /// Returns a neuron id randomly chosen from inner neurons unioned with output neurons. - /// This is all the places where a gene can end, aka go to. - /// Takes an optional "not" value, which, if supplied, will prevent this from returning - /// that value. - pub fn random_to_neuron(&self, not_id: Option) -> usize { - let num_neurons = self.inner_neurons.len() + self.output_neurons.len(); - let idx = thread_rng().gen_range(0..num_neurons); - - let id: usize; - - if idx < self.inner_neurons.len() { - let ids = &self.inner_neurons.keys().map(|k| *k).collect(); - id = get_id_not_id(ids, idx, not_id); - } else { - let ids = &self.output_neurons.keys().map(|k| *k).collect(); - let index = idx - self.inner_neurons.len(); - id = get_id_not_id(ids, index, not_id); - } - - id - } - - pub fn neuron_type(&self, neuron_id: &usize) -> &NeuronType { - &self.neuron_type_map[neuron_id] - } - - /// Add an inner neuron after the net is initially created. Initially made for mutation - pub fn add_inner_neuron(&mut self) { - - let id; - - if self.inner_neurons.len() == 0 { - id = 200; - } else { - id = self.inner_neurons.keys().max().unwrap() + &1; - } - - self.inner_neurons.insert(id, InnerNeuron { id }); - self.neuron_type_map.insert(id, NeuronType::InnerNeuron); - } - - /// Remove an inner neuron after the net is initially created. Initially made for mutation - pub fn remove_inner_neuron(&mut self, id: usize) { - self.inner_neurons.remove(&id); - self.neuron_type_map.remove(&id); - } +// Define a trait that describes how to calculate the area of a shape +trait Shape { + fn area(&self) -> f64; } -#[derive(Debug, EnumIter, Clone, Display, Serialize, Deserialize)] -pub enum InputNeuronType { - VisionDistanceLeft, - VisionTypeLeft, - VisionDistanceCenter, - VisionTypeCenter, - VisionDistanceRight, - VisionTypeRight, - PharamoneRedLeft, - PharamoneRedRight, - PharamoneYellowRight, - PharamoneYellowLeft, - PharamoneBlueRight, - PharamoneBlueLeft, - Health, - Hunger, - PopulationDensity, - NeighborhoodDensity, - Random, - Oscillator, +// Implement the Shape trait for a Circle +struct Circle { + radius: f64, } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct InputNeuron { - pub id: usize, - pub value: f32, +impl Shape for Circle { + fn area(&self) -> f64 { + std::f64::consts::PI * self.radius.powi(2) + } } -#[derive(Debug, EnumIter, Clone, Display, Serialize, Deserialize)] -pub enum OutputNeuronType { - TurnLeft, - TurnRight, - MoveForward, - Attack, - ExcretePharamoneRed, - ExcretePharamoneYellow, - ExcretePharamoneBlue, +// Implement the Shape trait for a Rectangle +struct Rectangle { + width: f64, + height: f64, } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct OutputNeuron { - pub id: usize, +impl Shape for Rectangle { + fn area(&self) -> f64 { + self.width * self.height + } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InnerNeuron { - pub id: usize, +fn calculate_area(shape: &dyn Shape) -> f64 { + shape.area() } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum NeuronType { - InputNeuron, - InnerNeuron, - OutputNeuron, +// Define a function that uses the Shape trait to print the area of an object +fn print_shape_area(shape: &dyn Shape) { + println!("The area of this shape is: {}", calculate_area(shape)); } -/// The basic issue is that we want to request a random neuron id, but we sometimes want -/// to make sure that it's different from a given one, which in this case is called not_id. -/// This is just a helper to abstract some of the repeated logic in random_{from,to}_neuron. -fn get_id_not_id(ids: &Vec, mut idx: usize, not_id: Option) -> usize { - let mut id = ids[idx]; +fn main() { + let circle = Circle { radius: 5.0 }; + let rectangle = Rectangle { width: 4.0, height: 6.0 }; - if let Some(not_id) = not_id { - if not_id == id { - if idx > 0 { - idx -= 1; - } else { - idx += 1; - } - } - } + print_shape_area(&circle); + print_shape_area(&rectangle); - // On some occasions we may have a length one vector. - // In these cases, we'll just return the first id. - if idx >= ids.len() { - idx = 0; + // Example of a function that uses both the Shape and Display traits + fn display_area(shape: &T) { + println!("The area of this {} is: {}", shape, calculate_area(shape)); } - id = ids[idx]; - - id -} - -#[cfg(test)] -mod test { - - use super::*; + display_area(&circle); + display_area(&rectangle); - #[test] - fn add_inner_neuron() { - let mut nn = NeuralNet::new(0); - assert_eq!(nn.inner_neurons.len(), 0); - nn.add_inner_neuron(); - assert_eq!(nn.inner_neurons.len(), 1); + // Example of a function that uses the Shape trait with generics + fn sum_areas(shapes: Vec<&T>) -> f64 { + shapes.iter().map(|shape| shape.area()).sum() } - #[test] - fn remove_inner_neuron() { - let mut nn = NeuralNet::new(1); - - assert_eq!(nn.inner_neurons.len(), 1); - - let id = nn.inner_neurons.keys().last().unwrap(); - nn.remove_inner_neuron(*id); - - assert_eq!(nn.inner_neurons.len(), 0); - } + let shapes = vec![&circle, &rectangle]; + println!("The total area is: {}", sum_areas(shapes)); } - diff --git a/tests/fixtures/scheme.scm b/tests/fixtures/scheme.scm index c5e7d0b..03d7d7a 100644 --- a/tests/fixtures/scheme.scm +++ b/tests/fixtures/scheme.scm @@ -1,8 +1,49 @@ -(define (permutations lst) - (if (null? lst) - '(()) - (append-map (lambda (x) - (map (lambda (y) (cons x y)) - (permutations (remove x lst)))) - lst))) +(define (print-odd-numbers up-to) + (let ((count 1)) + (lambda () + (if (> count up-to) + null + (begin + (display count) + (newline) + (set! count (+ count 2)) + (cons count (print-odd-numbers up-to))))))) +(define (sum-a-list lst) + (cond ((null? lst) 0) + ((list? (cdr lst)) + (+ (car lst) (sum-a-list (cdr lst)))) + (else + (display "Invalid list") + (newline)))) + +(define (is-prime? num) + (let ((divisors 2)) + (cond ((> (* divisors divisors) num) true) + ((= (modulo num divisors) 0) false) + (else + (set! divisors (+ divisors 1)) + (is-prime? num))))) + +(define (greet name) + (display "Hello, ") + (display name) + (newline)) + +(module test racket + (require rackunit) + + (define odd-numbers-upto-10 (print-odd-numbers 10)) + (check-equal? (car (odd-numbers-upto-10)) 1) + + (define sum-of-list '(1 2 3 4 5) + (sum-a-list sum-of-list) => 15) + + (define is-prime 100 + (is-prime? num) => #t)) + +(module main racket + (require rackunit) + + (let ((name "John")) + (greet name))) diff --git a/tests/treewalker/highlight_spec.lua b/tests/treewalker/highlight_spec.lua new file mode 100644 index 0000000..63550ef --- /dev/null +++ b/tests/treewalker/highlight_spec.lua @@ -0,0 +1,88 @@ +local util = require "treewalker.util" +local load_fixture = require "tests.load_fixture" +local stub = require 'luassert.stub' +local assert = require "luassert" +local treewalker = require 'treewalker' +local ops = require 'treewalker.ops' + +describe("Treewalker highlighting", function() + local highlight_stub = stub(ops, "highlight") + + -- use with rows as they're numbered in vim lines (1-indexed) + local function assert_highlighted(srow, scol, erow, ecol, desc) + assert.same( + { srow - 1, scol - 1, erow - 1, ecol }, + highlight_stub.calls[1].refs[1], + "highlight wrong for: " .. desc + ) + end + + describe("regular lua file: ", function() + load_fixture("/lua.lua", "lua") + + before_each(function() + treewalker.setup({ highlight = true }) + highlight_stub = stub(ops, "highlight") + end) + + it("respects highlight config option", function() + treewalker.setup() -- highlight defaults to true, doesn't blow up with empty setup + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.equal(4, #highlight_stub.calls) + + highlight_stub = stub(ops, "highlight") + treewalker.setup({ highlight = false }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.equal(0, #highlight_stub.calls) + + highlight_stub = stub(ops, "highlight") + treewalker.setup({ highlight = true }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.equal(4, #highlight_stub.calls) + end) + + it("highlights whole functions", function() + vim.fn.cursor(10, 1) + treewalker.move_down() + assert_highlighted(21, 1, 28, 3, "is_jump_target function") + end) + + it("highlights whole lines starting with identifiers", function() + vim.fn.cursor(134, 5) + treewalker.move_up() + assert_highlighted(133, 5, 133, 33, "table.insert call") + end) + + it("highlights whole lines starting assignments", function() + vim.fn.cursor(133, 5) + treewalker.move_down() + assert_highlighted(134, 5, 134, 18, "child = iter()") + end) + + -- Note this is highly language dependent, so this test is not so powerful + it("doesn't highlight the whole file", function() + vim.fn.cursor(3, 1) + treewalker.move_up() + assert_highlighted(1, 1, 1, 39, "first line") + end) + + -- Also very language dependent + it("highlights only the first item in a block", function() + vim.fn.cursor(27, 3) + treewalker.move_up() + assert_highlighted(22, 3, 26, 5, "child = iter()") + end) + end) +end) diff --git a/tests/treewalker/acceptance_spec.lua b/tests/treewalker/movement_spec.lua similarity index 74% rename from tests/treewalker/acceptance_spec.lua rename to tests/treewalker/movement_spec.lua index 6dfe34a..b8f7a19 100644 --- a/tests/treewalker/acceptance_spec.lua +++ b/tests/treewalker/movement_spec.lua @@ -18,7 +18,7 @@ local function assert_cursor_at(line, column, msg) assert.are.same({ line, column }, { current_line, current_column }, msg) end -describe("Treewalker", function() +describe("Treewalker movement", function() describe("regular lua file: ", function() load_fixture("/lua.lua", "lua") @@ -63,13 +63,13 @@ describe("Treewalker", function() end) it("doesn't jump into a comment", function() - vim.fn.cursor(177, 1) -- In a bigger function + vim.fn.cursor(177, 1) treewalker.move_in() assert_cursor_at(179, 3, "local") end) it("goes out of functions", function() - vim.fn.cursor(149, 7) -- In a bigger function + vim.fn.cursor(149, 7) treewalker.move_out() assert_cursor_at(148, 5, "if") treewalker.move_out() @@ -77,35 +77,6 @@ describe("Treewalker", function() treewalker.move_out() assert_cursor_at(143, 1, "function") end) - - it("respects highlight config option", function() - local highlight_stub = stub(ops, "highlight") - treewalker.setup() -- highlight defaults to true, doesn't blow up with empty setup - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.equal(4, #highlight_stub.calls) - - highlight_stub = stub(ops, "highlight") - treewalker.setup({ highlight = false }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.equal(0, #highlight_stub.calls) - - highlight_stub = stub(ops, "highlight") - treewalker.setup({ highlight = true }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.equal(4, #highlight_stub.calls) - end) end) describe("lua spec file: ", function() From f335bc715ebd5ca274876ca4829a6c130ddcd3f0 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 22:43:33 -0800 Subject: [PATCH 10/11] Update README because I read one line, so there was 1 line I wanted to change. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52f5958..cdb205e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Moving slowly, showing each command { "aaronik/treewalker.nvim", opts = { - highlight = true -- briefly highlight the node after jumping to it + highlight = true -- Whether to briefly highlight the node after jumping to it } } ``` From e30df9d6b2570d68fa4b645bbd60d1ddfb0a6077 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 23:52:53 -0800 Subject: [PATCH 11/11] Update readme badges --- .github/workflows/test.yml | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad95f48..e454ce8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: mkdir -p .local/share/nvim/lazy/ mv plenary.nvim .local/share/nvim/lazy/ - - name: Run Tests + - name: Tests env: XDG_CONFIG_HOME: ${{ github.workspace }}/.config XDG_DATA_HOME: ${{ github.workspace }}/.local/share diff --git a/README.md b/README.md index cdb205e..f17c3dc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +![build status](https://github.com/aaronik/treewalker.nvim/actions/workflows/test.yml/badge.svg) Static Badge