diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e454ce8 --- /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: 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/README.md b/README.md index 81a10cb..9742d27 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 @@ -30,7 +31,8 @@ Moving slowly, showing each command { "aaronik/treewalker.nvim", opts = { - highlight = true -- default is false; can also be a duration + highlight = true -- Whether to briefly highlight the node after jumping to it + -- Can be numeric to specify highlight duration instead } } ``` diff --git a/lua/treewalker/init.lua b/lua/treewalker/init.lua index 8a2365a..be56fad 100644 --- a/lua/treewalker/init.lua +++ b/lua/treewalker/init.lua @@ -8,8 +8,11 @@ local Treewalker = {} ---@alias Opts { highlight: boolean | integer } +-- Default setup() options ---@type Opts -Treewalker.opts = {} +Treewalker.opts = { + highlight = true +} ---@param opts Opts | nil function Treewalker.setup(opts) diff --git a/lua/treewalker/nodes.lua b/lua/treewalker/nodes.lua index 7fdc6db..a0476b8 100644 --- a/lua/treewalker/nodes.lua +++ b/lua/treewalker/nodes.lua @@ -1,36 +1,47 @@ local util = require "treewalker.util" -local lines= require "treewalker.lines" +local lines = require "treewalker.lines" -local NON_TARGET_NODE_MATCHERS = { - -- "chunk", -- lua - "^.*comment.*$", +-- These are regexes but just happen to be real simple so far +local TARGET_BLACKLIST_TYPE_MATCHERS = { + "comment", } -local TARGET_DESCENDANT_TYPES = { - "body_statement", -- lua, rb - "block", -- lua - "statement_block", -- lua - - -- "then", -- helps rb, hurts lua - "do_block", -- rb +local HIGHLIGHT_BLACKLIST_TYPE_MATCHERS = { + "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 } + local M = {} ---@param node TSNode +---@param matchers string[] ---@return boolean -function M.is_jump_target(node) - for _, matcher in ipairs(NON_TARGET_NODE_MATCHERS) do - -- If it's a banned type +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 -function M.is_descendant_jump_target(node) - return util.contains(TARGET_DESCENDANT_TYPES, node:type()) +---@param node TSNode +---@return boolean +function M.is_highlight_target(node) + return not is_matched_in(node, HIGHLIGHT_BLACKLIST_TYPE_MATCHERS) end ---Do the nodes have the same starting point @@ -45,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 @@ -83,41 +102,54 @@ 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 row +---@param node TSNode +---@return TSNode +function M.get_highest_coincident(node) + local parent = node:parent() + -- 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 + 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 5331255..228893d 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 ---@param duration integer @@ -65,6 +58,7 @@ function M.jump(row, node) vim.cmd('normal! ^') local highlight = require("treewalker").opts.highlight if highlight and highlight ~= 0 then + node = nodes.get_highest_coincident(node) M.highlight(nodes.range(node), highlight ~= true and highlight or 250) end end 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/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/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/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/highlight_spec.lua b/tests/treewalker/highlight_spec.lua new file mode 100644 index 0000000..d3d9dee --- /dev/null +++ b/tests/treewalker/highlight_spec.lua @@ -0,0 +1,134 @@ +local util = require "treewalker.util" +local load_fixture = require "tests.load_fixture" +local spy = require 'luassert.spy' +local assert = require "luassert" +local treewalker = require 'treewalker' +local ops = require 'treewalker.ops' + +describe("Treewalker highlighting", function() + local highlight_spy = spy.on(ops, "highlight") + local bufclear_spy = spy.on(vim.api, "nvim_buf_clear_namespace") + + -- 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_spy.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_spy = spy.on(ops, "highlight") + bufclear_spy = spy.on(vim.api, "nvim_buf_clear_namespace") + end) + + it("respects highlight config option", function() + vim.wait(250 + 5) -- wait out potential "buf_clear" calls queue up from previous tests + + -- 'nvim_buf_clear_namespace' should be called times + -- within a 10ms tolerance window after ms + local function assert_bufclears_after(timeout, calls) + bufclear_spy:clear() + vim.wait(timeout - 5) + assert.spy(bufclear_spy).was.not_called() + vim.wait(10) + assert.spy(bufclear_spy).was.called(calls) + end + + highlight_spy:clear() + 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.spy(highlight_spy).was.called(4) + assert_bufclears_after(250, 4) + + highlight_spy:clear() + treewalker.setup({ highlight = 0 }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.spy(highlight_spy).was.not_called() + + highlight_spy:clear() + treewalker.setup({ highlight = false }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.spy(highlight_spy).was.not_called() + + highlight_spy:clear() + treewalker.setup({ highlight = true }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.spy(highlight_spy).was.called(4) + assert_bufclears_after(250, 4) + + highlight_spy:clear() + treewalker.setup({ highlight = 50 }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.spy(highlight_spy).was.called(4) + assert_bufclears_after(50, 4) + + highlight_spy:clear() + treewalker.setup({ highlight = 500 }) + vim.fn.cursor(23, 5) + treewalker.move_out() + treewalker.move_down() + treewalker.move_up() + treewalker.move_in() + assert.spy(highlight_spy).was.called(4) + assert_bufclears_after(500, 4) + 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 59% rename from tests/treewalker/acceptance_spec.lua rename to tests/treewalker/movement_spec.lua index 62feccd..39f7b69 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,72 +77,6 @@ describe("Treewalker", function() treewalker.move_out() assert_cursor_at(143, 1, "function") end) - - it("respects highlight config option", function() - spy.on(ops, "highlight") - local bufclear = spy.on(vim.api, "nvim_buf_clear_namespace") - - -- 'nvim_buf_clear_namespace' should be called times - -- within a 10ms tolerance window after ms - local function assert_bufclears_after(timeout, calls) - bufclear:clear() - vim.wait(timeout - 5) - assert.spy(bufclear).was.not_called() - vim.wait(10) - assert.spy(bufclear).was.called(calls) - end - - treewalker.setup() - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.spy(ops.highlight).was.not_called() - - treewalker.setup({ highlight = 0 }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.spy(ops.highlight).was.not_called() - - treewalker.setup({ highlight = false }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.spy(ops.highlight).was.not_called() - - treewalker.setup({ highlight = true }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.spy(ops.highlight).was.called(4) - assert_bufclears_after(250, 4) - - treewalker.setup({ highlight = 50 }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.spy(ops.highlight).was.called(8) - assert_bufclears_after(50, 4) - - treewalker.setup({ highlight = 500 }) - vim.fn.cursor(23, 5) - treewalker.move_out() - treewalker.move_down() - treewalker.move_up() - treewalker.move_in() - assert.spy(ops.highlight).was.called(12) - assert_bufclears_after(500, 4) - end) end) describe("lua spec file: ", function() 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 }