From 60b6ff357ff01793e155910cac30dc0a45466418 Mon Sep 17 00:00:00 2001 From: Aaron Sullivan Date: Sun, 15 Dec 2024 22:37:27 -0800 Subject: [PATCH] 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()