From 956a0a4b3f4e74feb82c8aeada2d05fdbf229103 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Wed, 11 Dec 2024 14:25:45 +0100 Subject: [PATCH 1/9] feat(TreeCursor) : added method to retrieve current node with custom allocator --- .../treesitter/jtreesitter/TreeCursor.java | 52 ++++++++----------- .../jtreesitter/TreeCursorTest.java | 21 +++++++- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java b/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java index 18be2b3..4ca189c 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java @@ -4,6 +4,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.lang.foreign.SegmentAllocator; import java.util.OptionalInt; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -14,7 +15,6 @@ public final class TreeCursor implements AutoCloseable, Cloneable { private final MemorySegment self; private final Arena arena; private final Tree tree; - private @Nullable Node node; TreeCursor(Node node, Tree tree) { arena = Arena.ofShared(); @@ -33,16 +33,25 @@ private TreeCursor(TreeCursor cursor) { arena = Arena.ofShared(); self = ts_tree_cursor_copy(arena, cursor.self); tree = cursor.tree.clone(); - node = cursor.node; } - /** Get the current node of the cursor. */ + /** + * Get the current node of the cursor. Its native memory will be managed by the cursor. It will become invalid + * once the cursor is closed by calling {@link #close()}. + * @return the current node + */ public Node getCurrentNode() { - if (this.node == null) { - var node = ts_tree_cursor_current_node(arena, self); - this.node = new Node(node, tree); - } - return this.node; + return getCurrentNode(arena); + } + + /** + * Get the current node of the cursor. Its native memory will be managed by the given allocator. + * @param allocator the allocator to use for managing the native memory of the node + * @return the current node + */ + public Node getCurrentNode(SegmentAllocator allocator) { + var node = ts_tree_cursor_current_node(allocator, self); + return new Node(node, tree); } /** @@ -88,9 +97,7 @@ public Node getCurrentNode() { * {@code false} if there were no children. */ public boolean gotoFirstChild() { - var result = ts_tree_cursor_goto_first_child(self); - if (result) node = null; - return result; + return ts_tree_cursor_goto_first_child(self); } /** @@ -100,9 +107,7 @@ public boolean gotoFirstChild() { * {@code false} if there were no children. */ public boolean gotoLastChild() { - var result = ts_tree_cursor_goto_last_child(self); - if (result) node = null; - return result; + return ts_tree_cursor_goto_last_child(self); } /** @@ -112,9 +117,7 @@ public boolean gotoLastChild() { * {@code false} if there was no parent node. */ public boolean gotoParent() { - var result = ts_tree_cursor_goto_parent(self); - if (result) node = null; - return result; + return ts_tree_cursor_goto_parent(self); } /** @@ -124,9 +127,7 @@ public boolean gotoParent() { * {@code false} if there was no next sibling node. */ public boolean gotoNextSibling() { - var result = ts_tree_cursor_goto_next_sibling(self); - if (result) node = null; - return result; + return ts_tree_cursor_goto_next_sibling(self); } /** @@ -136,9 +137,7 @@ public boolean gotoNextSibling() { * {@code false} if there was no previous sibling node. */ public boolean gotoPreviousSibling() { - var result = ts_tree_cursor_goto_previous_sibling(self); - if (result) node = null; - return result; + return ts_tree_cursor_goto_previous_sibling(self); } /** @@ -149,7 +148,7 @@ public boolean gotoPreviousSibling() { */ public void gotoDescendant(@Unsigned int index) { ts_tree_cursor_goto_descendant(self, index); - node = null; + } /** @@ -161,7 +160,6 @@ public void gotoDescendant(@Unsigned int index) { public @Unsigned OptionalInt gotoFirstChildForByte(@Unsigned int offset) { var index = ts_tree_cursor_goto_first_child_for_byte(self, offset); if (index == -1L) return OptionalInt.empty(); - node = null; return OptionalInt.of((int) index); } @@ -176,7 +174,6 @@ public void gotoDescendant(@Unsigned int index) { var goal = point.into(arena); var index = ts_tree_cursor_goto_first_child_for_point(self, goal); if (index == -1L) return OptionalInt.empty(); - node = null; return OptionalInt.of((int) index); } } @@ -185,15 +182,12 @@ public void gotoDescendant(@Unsigned int index) { public void reset(Node node) { try (var arena = Arena.ofConfined()) { ts_tree_cursor_reset(self, node.copy(arena)); - } finally { - this.node = null; } } /** Reset the cursor to start at the same position as another cursor. */ public void reset(TreeCursor cursor) { ts_tree_cursor_reset_to(self, cursor.self); - this.node = null; } /** Create a shallow copy of the tree cursor. */ diff --git a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java index 899f8d1..2548fed 100644 --- a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java +++ b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java @@ -5,6 +5,8 @@ import io.github.treesitter.jtreesitter.languages.TreeSitterJava; import org.junit.jupiter.api.*; +import java.lang.foreign.Arena; + class TreeCursorTest { private static Tree tree; private TreeCursor cursor; @@ -29,14 +31,29 @@ void setUp() { @AfterEach void tearDown() { - cursor.close(); + if(cursor != null){ + cursor.close(); + } } @Test void getCurrentNode() { var node = cursor.getCurrentNode(); assertEquals(tree.getRootNode(), node); - assertSame(node, cursor.getCurrentNode()); + } + + @Test + void getCurrentNodeWithCustomAllocator() { + + try(var arena = Arena.ofConfined()){ + var node = cursor.getCurrentNode(arena); + assertEquals(tree.getRootNode(), node); + cursor.close(); + cursor = null; // avoid double close + // can still access node after cursor was closed + assertEquals(tree.getRootNode(), node); + } + } @Test From f078e973bc8b82d00c3d37d33fdbcbb6a52820c2 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Wed, 11 Dec 2024 14:27:02 +0100 Subject: [PATCH 2/9] feat(Query) : added method to find matches with custom allocator --- .../github/treesitter/jtreesitter/Query.java | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/treesitter/jtreesitter/Query.java b/src/main/java/io/github/treesitter/jtreesitter/Query.java index 690b751..4095327 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/Query.java +++ b/src/main/java/io/github/treesitter/jtreesitter/Query.java @@ -5,6 +5,7 @@ import io.github.treesitter.jtreesitter.internal.*; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.lang.foreign.SegmentAllocator; import java.util.*; import java.util.function.BiPredicate; import java.util.function.Consumer; @@ -478,16 +479,19 @@ public Map> getPatternAssertions(@Unsigned int index, b } /** - * Iterate over all the matches in the order that they were found. + * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned + * matches is bound to the lifetime of this query object. * * @param node The node that the query will run on. */ public Stream findMatches(Node node) { - return findMatches(node, null); + return findMatches(node, null, arena); } + /** - * Iterate over all the matches in the order that they were found. + * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned + * matches is bound to the lifetime of this query object. * *

Predicate Example

*

@@ -503,11 +507,24 @@ public Stream findMatches(Node node) { * @param node The node that the query will run on. * @param predicate A function that handles custom predicates. */ - public Stream findMatches(Node node, @Nullable BiPredicate predicate) { + public Stream findMatches(Node node, @Nullable BiPredicate predicate){ + return findMatches(node, predicate, arena); + } + + + /** + * Like {@link #findMatches(Node, BiPredicate)} but the native memory of the returned matches is created using the + * given allocator. + * + * @param node The node that the query will run on. + * @param predicate A function that handles custom predicates. + * @param allocator The allocator that is used to allocate the native memory of the returned matches. + */ + public Stream findMatches(Node node, @Nullable BiPredicate predicate, SegmentAllocator allocator) { try (var alloc = Arena.ofConfined()) { ts_query_cursor_exec(cursor, query, node.copy(alloc)); } - return StreamSupport.stream(new MatchesIterator(node.getTree(), predicate), false); + return StreamSupport.stream(new MatchesIterator(node.getTree(), predicate, allocator), false); } @Override @@ -537,11 +554,13 @@ private void checkIndex(@Unsigned int index) throws IndexOutOfBoundsException { private final class MatchesIterator extends Spliterators.AbstractSpliterator { private final @Nullable BiPredicate predicate; private final Tree tree; + private final SegmentAllocator allocator; - public MatchesIterator(Tree tree, @Nullable BiPredicate predicate) { + public MatchesIterator(Tree tree, @Nullable BiPredicate predicate, SegmentAllocator allocator) { super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); this.predicate = predicate; this.tree = tree; + this.allocator = allocator; } @Override @@ -555,7 +574,7 @@ public boolean tryAdvance(Consumer action) { for (int i = 0; i < count; ++i) { var capture = TSQueryCapture.asSlice(matchCaptures, i); var name = captureNames.get(TSQueryCapture.index(capture)); - var node = TSNode.allocate(arena).copyFrom(TSQueryCapture.node(capture)); + var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture)); captureList.add(new QueryCapture(name, new Node(node, tree))); } var patternIndex = TSQueryMatch.pattern_index(match); From ae27c2e8e22627fa16cb3b0a959be3b2f0d88938 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 12 Dec 2024 15:03:09 +0100 Subject: [PATCH 3/9] added back cached node in TreeCursor rebase this --- .../treesitter/jtreesitter/TreeCursor.java | 36 +++++++++++++++---- .../jtreesitter/TreeCursorTest.java | 1 + 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java b/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java index 4ca189c..08feea1 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java @@ -15,6 +15,7 @@ public final class TreeCursor implements AutoCloseable, Cloneable { private final MemorySegment self; private final Arena arena; private final Tree tree; + private @Nullable Node node; TreeCursor(Node node, Tree tree) { arena = Arena.ofShared(); @@ -33,6 +34,7 @@ private TreeCursor(TreeCursor cursor) { arena = Arena.ofShared(); self = ts_tree_cursor_copy(arena, cursor.self); tree = cursor.tree.clone(); + node = cursor.node; } /** @@ -41,9 +43,14 @@ private TreeCursor(TreeCursor cursor) { * @return the current node */ public Node getCurrentNode() { - return getCurrentNode(arena); + if (this.node == null) { + var node = ts_tree_cursor_current_node(arena, self); + this.node = new Node(node, tree); + } + return this.node; } + /** * Get the current node of the cursor. Its native memory will be managed by the given allocator. * @param allocator the allocator to use for managing the native memory of the node @@ -97,7 +104,9 @@ public Node getCurrentNode(SegmentAllocator allocator) { * {@code false} if there were no children. */ public boolean gotoFirstChild() { - return ts_tree_cursor_goto_first_child(self); + var result = ts_tree_cursor_goto_first_child(self); + if (result) node = null; + return result; } /** @@ -107,7 +116,9 @@ public boolean gotoFirstChild() { * {@code false} if there were no children. */ public boolean gotoLastChild() { - return ts_tree_cursor_goto_last_child(self); + var result = ts_tree_cursor_goto_last_child(self); + if (result) node = null; + return result; } /** @@ -117,7 +128,9 @@ public boolean gotoLastChild() { * {@code false} if there was no parent node. */ public boolean gotoParent() { - return ts_tree_cursor_goto_parent(self); + var result = ts_tree_cursor_goto_parent(self); + if (result) node = null; + return result; } /** @@ -127,7 +140,9 @@ public boolean gotoParent() { * {@code false} if there was no next sibling node. */ public boolean gotoNextSibling() { - return ts_tree_cursor_goto_next_sibling(self); + var result = ts_tree_cursor_goto_next_sibling(self); + if (result) node = null; + return result; } /** @@ -137,7 +152,9 @@ public boolean gotoNextSibling() { * {@code false} if there was no previous sibling node. */ public boolean gotoPreviousSibling() { - return ts_tree_cursor_goto_previous_sibling(self); + var result = ts_tree_cursor_goto_previous_sibling(self); + if (result) node = null; + return result; } /** @@ -148,7 +165,7 @@ public boolean gotoPreviousSibling() { */ public void gotoDescendant(@Unsigned int index) { ts_tree_cursor_goto_descendant(self, index); - + node = null; } /** @@ -160,6 +177,7 @@ public void gotoDescendant(@Unsigned int index) { public @Unsigned OptionalInt gotoFirstChildForByte(@Unsigned int offset) { var index = ts_tree_cursor_goto_first_child_for_byte(self, offset); if (index == -1L) return OptionalInt.empty(); + node = null; return OptionalInt.of((int) index); } @@ -174,6 +192,7 @@ public void gotoDescendant(@Unsigned int index) { var goal = point.into(arena); var index = ts_tree_cursor_goto_first_child_for_point(self, goal); if (index == -1L) return OptionalInt.empty(); + node = null; return OptionalInt.of((int) index); } } @@ -182,12 +201,15 @@ public void gotoDescendant(@Unsigned int index) { public void reset(Node node) { try (var arena = Arena.ofConfined()) { ts_tree_cursor_reset(self, node.copy(arena)); + } finally { + this.node = null; } } /** Reset the cursor to start at the same position as another cursor. */ public void reset(TreeCursor cursor) { ts_tree_cursor_reset_to(self, cursor.self); + this.node = null; } /** Create a shallow copy of the tree cursor. */ diff --git a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java index 2548fed..11c562b 100644 --- a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java +++ b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java @@ -40,6 +40,7 @@ void tearDown() { void getCurrentNode() { var node = cursor.getCurrentNode(); assertEquals(tree.getRootNode(), node); + assertSame(node, cursor.getCurrentNode()); } @Test From f60959d215ea65d77a930bccec3040685d971605 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 12 Dec 2024 15:03:36 +0100 Subject: [PATCH 4/9] cloning cursor instead of special closing logic --- .../treesitter/jtreesitter/TreeCursorTest.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java index 11c562b..f49f978 100644 --- a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java +++ b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java @@ -31,9 +31,7 @@ void setUp() { @AfterEach void tearDown() { - if(cursor != null){ - cursor.close(); - } + cursor.close(); } @Test @@ -45,16 +43,15 @@ void getCurrentNode() { @Test void getCurrentNodeWithCustomAllocator() { - - try(var arena = Arena.ofConfined()){ - var node = cursor.getCurrentNode(arena); - assertEquals(tree.getRootNode(), node); - cursor.close(); - cursor = null; // avoid double close + try (var arena = Arena.ofConfined()) { + Node node; + try (TreeCursor copied = cursor.clone()) { + node = copied.getCurrentNode(arena); + assertEquals(tree.getRootNode(), node); + } // can still access node after cursor was closed assertEquals(tree.getRootNode(), node); } - } @Test From c86f5aecf035dcc6ab34cfe894b3c07d5c597046 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 2 Jan 2025 08:25:43 +0100 Subject: [PATCH 5/9] reorder parameters to match kotlin convention --- src/main/java/io/github/treesitter/jtreesitter/Query.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/treesitter/jtreesitter/Query.java b/src/main/java/io/github/treesitter/jtreesitter/Query.java index 4095327..fb64d80 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/Query.java +++ b/src/main/java/io/github/treesitter/jtreesitter/Query.java @@ -485,7 +485,7 @@ public Map> getPatternAssertions(@Unsigned int index, b * @param node The node that the query will run on. */ public Stream findMatches(Node node) { - return findMatches(node, null, arena); + return findMatches(node, arena, null); } @@ -508,7 +508,7 @@ public Stream findMatches(Node node) { * @param predicate A function that handles custom predicates. */ public Stream findMatches(Node node, @Nullable BiPredicate predicate){ - return findMatches(node, predicate, arena); + return findMatches(node, arena, predicate); } @@ -517,10 +517,10 @@ public Stream findMatches(Node node, @Nullable BiPredicate findMatches(Node node, @Nullable BiPredicate predicate, SegmentAllocator allocator) { + public Stream findMatches(Node node, SegmentAllocator allocator, @Nullable BiPredicate predicate) { try (var alloc = Arena.ofConfined()) { ts_query_cursor_exec(cursor, query, node.copy(alloc)); } From 7b29a001e0a216fa92a50cbcd171303e4f61613c Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 2 Jan 2025 11:41:35 +0100 Subject: [PATCH 6/9] experimental Query / QueryCapture separation --- .../github/treesitter/jtreesitter/Query.java | 103 +++------ .../treesitter/jtreesitter/QueryCursor.java | 200 ++++++++++++++++++ .../jtreesitter/QueryCursorOptions.java | 71 +++++++ .../treesitter/jtreesitter/QueryMatch.java | 7 +- 4 files changed, 310 insertions(+), 71 deletions(-) create mode 100644 src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java create mode 100644 src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java diff --git a/src/main/java/io/github/treesitter/jtreesitter/Query.java b/src/main/java/io/github/treesitter/jtreesitter/Query.java index fb64d80..7a638b9 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/Query.java +++ b/src/main/java/io/github/treesitter/jtreesitter/Query.java @@ -8,12 +8,11 @@ import java.lang.foreign.SegmentAllocator; import java.util.*; import java.util.function.BiPredicate; -import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Stream; -import java.util.stream.StreamSupport; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -26,7 +25,7 @@ @NullMarked public final class Query implements AutoCloseable { private final MemorySegment query; - private final MemorySegment cursor; + private final QueryCursorOptions cursorOptions = new QueryCursorOptions(); private final Arena arena; private final Language language; private final String source; @@ -86,7 +85,6 @@ public final class Query implements AutoCloseable { this.language = language; this.source = source; this.query = query.reinterpret(arena, TreeSitter::ts_query_delete); - cursor = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete); var captureCount = ts_query_capture_count(this.query); captureNames = new ArrayList<>(captureCount); @@ -263,6 +261,10 @@ private static boolean invalidPredicateChar(char c) { return !(Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == '?' || c == '!'); } + MemorySegment self(){ + return query; + } + /** Get the number of patterns in the query. */ public @Unsigned int getPatternCount() { return ts_query_pattern_count(query); @@ -273,13 +275,21 @@ private static boolean invalidPredicateChar(char c) { return ts_query_capture_count(query); } + public List> getPredicates(){ + return predicates.stream().map(Collections::unmodifiableList).toList(); + } + + public List getCaptureNames(){ + return Collections.unmodifiableList(captureNames); + } + /** * Get the maximum number of in-progress matches. * * @apiNote Defaults to {@code -1} (unlimited). */ public @Unsigned int getMatchLimit() { - return ts_query_cursor_match_limit(cursor); + return cursorOptions.getMatchLimit(); } /** @@ -288,10 +298,7 @@ private static boolean invalidPredicateChar(char c) { * @throws IllegalArgumentException If {@code matchLimit == 0}. */ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentException { - if (matchLimit == 0) { - throw new IllegalArgumentException("The match limit cannot equal 0"); - } - ts_query_cursor_set_match_limit(cursor, matchLimit); + cursorOptions.setMatchLimit(matchLimit); return this; } @@ -303,7 +310,7 @@ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentExcep * @since 0.23.1 */ public @Unsigned long getTimeoutMicros() { - return ts_query_cursor_timeout_micros(cursor); + return cursorOptions.getTimeoutMicros(); } /** @@ -313,7 +320,7 @@ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentExcep * @since 0.23.1 */ public Query setTimeoutMicros(@Unsigned long timeoutMicros) { - ts_query_cursor_set_timeout_micros(cursor, timeoutMicros); + cursorOptions.setTimeoutMicros(timeoutMicros); return this; } @@ -324,32 +331,24 @@ public Query setTimeoutMicros(@Unsigned long timeoutMicros) { *
Note that if a pattern includes many children, then they will still be checked. */ public Query setMaxStartDepth(@Unsigned int maxStartDepth) { - ts_query_cursor_set_max_start_depth(cursor, maxStartDepth); + cursorOptions.setMaxStartDepth(maxStartDepth); return this; } /** Set the range of bytes in which the query will be executed. */ public Query setByteRange(@Unsigned int startByte, @Unsigned int endByte) { - ts_query_cursor_set_byte_range(cursor, startByte, endByte); + cursorOptions.setStartByte(startByte); + cursorOptions.setEndByte(endByte); return this; } /** Set the range of points in which the query will be executed. */ public Query setPointRange(Point startPoint, Point endPoint) { - try (var alloc = Arena.ofConfined()) { - MemorySegment start = startPoint.into(alloc), end = endPoint.into(alloc); - ts_query_cursor_set_point_range(cursor, start, end); - } + cursorOptions.setStartPoint(startPoint); + cursorOptions.setEndPoint(endPoint); return this; } - /** - * Check if the query exceeded its maximum number of - * in-progress matches during its last execution. - */ - public boolean didExceedMatchLimit() { - return ts_query_cursor_did_exceed_match_limit(cursor); - } /** * Disable a certain pattern within a query. @@ -478,6 +477,16 @@ public Map> getPatternAssertions(@Unsigned int index, b return Collections.unmodifiableMap(assertions.get(index)); } + + public QueryCursor execute(Node node){ + return new QueryCursor(this, node, cursorOptions); + } + + public QueryCursor execute(Node node, QueryCursorOptions options){ + return new QueryCursor(this, node, options); + } + + /** * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned * matches is bound to the lifetime of this query object. @@ -521,10 +530,9 @@ public Stream findMatches(Node node, @Nullable BiPredicate findMatches(Node node, SegmentAllocator allocator, @Nullable BiPredicate predicate) { - try (var alloc = Arena.ofConfined()) { - ts_query_cursor_exec(cursor, query, node.copy(alloc)); + try(QueryCursor cursor = this.execute(node)){ + return cursor.stream(allocator, predicate); } - return StreamSupport.stream(new MatchesIterator(node.getTree(), predicate, allocator), false); } @Override @@ -537,12 +545,6 @@ public String toString() { return "Query{language=%s, source=%s}".formatted(language, source); } - private boolean matches(@Nullable BiPredicate predicate, QueryMatch match) { - return predicates.get(match.patternIndex()).stream().allMatch(p -> { - if (p.getClass() != QueryPredicate.class) return p.test(match); - return predicate == null || predicate.test(p, match); - }); - } private void checkIndex(@Unsigned int index) throws IndexOutOfBoundsException { if (Integer.compareUnsigned(index, getPatternCount()) >= 0) { @@ -551,40 +553,5 @@ private void checkIndex(@Unsigned int index) throws IndexOutOfBoundsException { } } - private final class MatchesIterator extends Spliterators.AbstractSpliterator { - private final @Nullable BiPredicate predicate; - private final Tree tree; - private final SegmentAllocator allocator; - public MatchesIterator(Tree tree, @Nullable BiPredicate predicate, SegmentAllocator allocator) { - super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); - this.predicate = predicate; - this.tree = tree; - this.allocator = allocator; - } - - @Override - public boolean tryAdvance(Consumer action) { - var hasNoText = tree.getText() == null; - MemorySegment match = arena.allocate(TSQueryMatch.layout()); - while (ts_query_cursor_next_match(cursor, match)) { - var count = Short.toUnsignedInt(TSQueryMatch.capture_count(match)); - var matchCaptures = TSQueryMatch.captures(match); - var captureList = new ArrayList(count); - for (int i = 0; i < count; ++i) { - var capture = TSQueryCapture.asSlice(matchCaptures, i); - var name = captureNames.get(TSQueryCapture.index(capture)); - var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture)); - captureList.add(new QueryCapture(name, new Node(node, tree))); - } - var patternIndex = TSQueryMatch.pattern_index(match); - var result = new QueryMatch(patternIndex, captureList); - if (hasNoText || matches(predicate, result)) { - action.accept(result); - return true; - } - } - return false; - } - } } diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java new file mode 100644 index 0000000..606f0b2 --- /dev/null +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java @@ -0,0 +1,200 @@ +package io.github.treesitter.jtreesitter; + +import io.github.treesitter.jtreesitter.internal.TSNode; +import io.github.treesitter.jtreesitter.internal.TSQueryCapture; +import io.github.treesitter.jtreesitter.internal.TSQueryMatch; +import io.github.treesitter.jtreesitter.internal.TreeSitter; +import org.jspecify.annotations.Nullable; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SegmentAllocator; +import java.util.ArrayList; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_did_exceed_match_limit; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_exec; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_match_limit; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_new; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_next_match; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_remove_match; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_byte_range; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_match_limit; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_max_start_depth; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_point_range; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_timeout_micros; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_timeout_micros; + +public class QueryCursor implements AutoCloseable{ + + private final MemorySegment cursor; + private final Arena arena; + + private final Query query; + private final Node cursorRootNode; + + + QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorOptions options){ + arena = Arena.ofConfined(); + cursor = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete); + this.query = query; + this.cursorRootNode = cursorRootNode; + if(options != null){ + applyOptions(options); + } + + try (var alloc = Arena.ofConfined()) { + ts_query_cursor_exec(cursor, query.self(), cursorRootNode.copy(alloc)); + } + } + + private void applyOptions(QueryCursorOptions options){ + + if (options.getStartByte() >= 0 && options.getEndByte() >= 0) { + ts_query_cursor_set_byte_range(cursor, options.getStartByte(), options.getEndByte()); + } + + if (options.getStartPoint() != null && options.getEndPoint() != null) { + try (var alloc = Arena.ofConfined()) { + MemorySegment start = options.getStartPoint().into(alloc); + MemorySegment end = options.getEndPoint().into(alloc); + ts_query_cursor_set_point_range(cursor, start, end); + } + } + + if (options.getMaxStartDepth() >= 0) { + ts_query_cursor_set_max_start_depth(cursor, options.getMaxStartDepth()); + } + + if (options.getMatchLimit() >= 0) { + ts_query_cursor_set_match_limit(cursor, options.getMatchLimit()); + } + + if (options.getTimeoutMicros() >= 0) { + ts_query_cursor_set_timeout_micros(cursor, options.getTimeoutMicros()); + } + + } + + + /** + * Get the maximum number of in-progress matches. + * + * @apiNote Defaults to {@code -1} (unlimited). + */ + public @Unsigned int getMatchLimit() { + return ts_query_cursor_match_limit(cursor); + } + + + /** + * Get the maximum duration in microseconds that query + * execution should be allowed to take before halting. + * + * @apiNote Defaults to {@code 0} (unlimited). + * @since 0.23.1 + */ + public @Unsigned long getTimeoutMicros() { + return ts_query_cursor_timeout_micros(cursor); + } + + + /** + * Check if the query exceeded its maximum number of + * in-progress matches during its last execution. + */ + public boolean didExceedMatchLimit() { + return ts_query_cursor_did_exceed_match_limit(cursor); + } + + public void removeMatch(@Unsigned int matchId){ + ts_query_cursor_remove_match(cursor, matchId); + } + + public Stream stream() { + return stream(null); + } + + public Stream stream(@Nullable BiPredicate predicate) { + return stream(arena , predicate); + } + + public Stream stream(SegmentAllocator allocator, @Nullable BiPredicate predicate) { + return StreamSupport.stream(new MatchesIterator(this, allocator, predicate), false); + } + + + public Optional nextMatch() { + return nextMatch(null); + } + + public Optional nextMatch(@Nullable BiPredicate predicate) { + return nextMatch(arena, predicate); + } + + public Optional nextMatch(SegmentAllocator allocator, @Nullable BiPredicate predicate) { + + var hasNoText = cursorRootNode.getTree().getText() == null; + MemorySegment match = arena.allocate(TSQueryMatch.layout()); + while (ts_query_cursor_next_match(cursor, match)) { + var count = Short.toUnsignedInt(TSQueryMatch.capture_count(match)); + var matchCaptures = TSQueryMatch.captures(match); + var captureList = new ArrayList(count); + for (int i = 0; i < count; ++i) { + var capture = TSQueryCapture.asSlice(matchCaptures, i); + var name = query.getCaptureNames().get(TSQueryCapture.index(capture)); + var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture)); + captureList.add(new QueryCapture(name, new Node(node, this.cursorRootNode.getTree()))); + } + var patternIndex = TSQueryMatch.pattern_index(match); + var matchId = TSQueryMatch.id(match); + var result = new QueryMatch(matchId, patternIndex, captureList); + if (hasNoText || matches(predicate, result)) { + return Optional.of(result); + } + } + return Optional.empty(); + } + + private boolean matches(@Nullable BiPredicate predicate, QueryMatch match) { + return query.getPredicates().get(match.patternIndex()).stream().allMatch(p -> { + if (p.getClass() != QueryPredicate.class) return p.test(match); + return predicate == null || predicate.test(p, match); + }); + } + + + @Override + public void close() { + arena.close(); + } + + private static final class MatchesIterator extends Spliterators.AbstractSpliterator { + + private final @Nullable BiPredicate predicate; + private final SegmentAllocator allocator; + + private final QueryCursor cursor; + + public MatchesIterator(QueryCursor cursor, SegmentAllocator allocator, @Nullable BiPredicate predicate) { + super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); + this.predicate = predicate; + this.allocator = allocator; + this.cursor = cursor; + } + @Override + public boolean tryAdvance(Consumer action) { + Optional queryMatch = cursor.nextMatch(allocator, predicate); + queryMatch.ifPresent(action); + return queryMatch.isPresent(); + } + + } + +} diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java new file mode 100644 index 0000000..36941bf --- /dev/null +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java @@ -0,0 +1,71 @@ +package io.github.treesitter.jtreesitter; + +public class QueryCursorOptions { + private int matchLimit = -1; // Default to unlimited + private long timeoutMicros = -1; // Default to unlimited + private int maxStartDepth = -1; + private int startByte = -1; + private int endByte = -1; + private Point startPoint; + private Point endPoint; + + + public int getMatchLimit() { + return matchLimit; + } + + public void setMatchLimit(int matchLimit) throws IllegalArgumentException { + if (matchLimit == 0) { + throw new IllegalArgumentException("The match limit cannot equal 0"); + } + this.matchLimit = matchLimit; + } + + public long getTimeoutMicros() { + return timeoutMicros; + } + + public void setTimeoutMicros(long timeoutMicros) { + this.timeoutMicros = timeoutMicros; + } + + public int getMaxStartDepth() { + return maxStartDepth; + } + + public void setMaxStartDepth(int maxStartDepth) { + this.maxStartDepth = maxStartDepth; + } + + public int getStartByte() { + return startByte; + } + + public void setStartByte(int startByte) { + this.startByte = startByte; + } + + public int getEndByte() { + return endByte; + } + + public void setEndByte(int endByte) { + this.endByte = endByte; + } + + public Point getStartPoint() { + return startPoint; + } + + public void setStartPoint(Point startPoint) { + this.startPoint = startPoint; + } + + public Point getEndPoint() { + return endPoint; + } + + public void setEndPoint(Point endPoint) { + this.endPoint = endPoint; + } +} diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java b/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java index 40ab7d7..b77f1af 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java @@ -5,9 +5,10 @@ /** A match that corresponds to a certain pattern in the query. */ @NullMarked -public record QueryMatch(@Unsigned int patternIndex, List captures) { +public record QueryMatch(int matchId, @Unsigned int patternIndex, List captures) { /** Creates an instance of a QueryMatch record class. */ - public QueryMatch(@Unsigned int patternIndex, List captures) { + public QueryMatch(@Unsigned int matchId, @Unsigned int patternIndex, List captures) { + this.matchId = matchId; this.patternIndex = patternIndex; this.captures = List.copyOf(captures); } @@ -23,6 +24,6 @@ public List findNodes(String capture) { @Override public String toString() { return String.format( - "QueryMatch[patternIndex=%s, captures=%s]", Integer.toUnsignedString(patternIndex), captures); + "QueryMatch[matchId=%s,patternIndex=%s, captures=%s]", Integer.toUnsignedString(matchId), Integer.toUnsignedString(patternIndex), captures); } } From 2172b243f2be0e1845cd3bfe4ff54046eb3c84b3 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Fri, 3 Jan 2025 08:44:57 +0100 Subject: [PATCH 7/9] refine Query / QueryCursor separation --- .../github/treesitter/jtreesitter/Query.java | 24 +++++++-- .../treesitter/jtreesitter/QueryCursor.java | 49 ++++++++++++++++--- .../treesitter/jtreesitter/QueryMatch.java | 8 +-- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/github/treesitter/jtreesitter/Query.java b/src/main/java/io/github/treesitter/jtreesitter/Query.java index 7a638b9..79bb6e3 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/Query.java +++ b/src/main/java/io/github/treesitter/jtreesitter/Query.java @@ -478,10 +478,21 @@ public Map> getPatternAssertions(@Unsigned int index, b } + /** + * Execute the query on a given node. + * @param node The node that the query will run on. + * @return A cursor that can be used to iterate over the matches. + */ public QueryCursor execute(Node node){ return new QueryCursor(this, node, cursorOptions); } + /** + * Execute the query on a given node with the given options. The options override the default options set on the query. + * @param node The node that the query will run on. + * @param options The options that will be used for this query. + * @return A cursor that can be used to iterate over the matches. + */ public QueryCursor execute(Node node, QueryCursorOptions options){ return new QueryCursor(this, node, options); } @@ -504,19 +515,20 @@ public Stream findMatches(Node node) { * *

Predicate Example

*

- * {@snippet lang="java" : + * {@snippet lang = "java": * Stream matches = query.findMatches(tree.getRootNode(), (predicate, match) -> { * if (!predicate.getName().equals("ieq?")) return true; * List args = predicate.getArgs(); * Node node = match.findNodes(args.getFirst().value()).getFirst(); * return args.getLast().value().equalsIgnoreCase(node.getText()); * }); - * } + *} * - * @param node The node that the query will run on. + * @param node The node that the query will run on. * @param predicate A function that handles custom predicates. + * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#stream(BiPredicate)}. */ - public Stream findMatches(Node node, @Nullable BiPredicate predicate){ + public Stream findMatches(Node node, @Nullable BiPredicate predicate) { return findMatches(node, arena, predicate); } @@ -531,7 +543,9 @@ public Stream findMatches(Node node, @Nullable BiPredicate findMatches(Node node, SegmentAllocator allocator, @Nullable BiPredicate predicate) { try(QueryCursor cursor = this.execute(node)){ - return cursor.stream(allocator, predicate); + // make sure to load the entire stream into memory before closing the cursor. + // Otherwise, we call for nextMatch after closing the cursor which leads to undefined behavior. + return cursor.stream(allocator, predicate).toList().stream(); } } diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java index 606f0b2..9202fd5 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java @@ -37,14 +37,14 @@ public class QueryCursor implements AutoCloseable{ private final Arena arena; private final Query query; - private final Node cursorRootNode; + private final Tree tree; QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorOptions options){ arena = Arena.ofConfined(); cursor = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete); this.query = query; - this.cursorRootNode = cursorRootNode; + this.tree = cursorRootNode.getTree(); if(options != null){ applyOptions(options); } @@ -79,7 +79,6 @@ private void applyOptions(QueryCursorOptions options){ if (options.getTimeoutMicros() >= 0) { ts_query_cursor_set_timeout_micros(cursor, options.getTimeoutMicros()); } - } @@ -117,30 +116,61 @@ public void removeMatch(@Unsigned int matchId){ ts_query_cursor_remove_match(cursor, matchId); } + /** + * Stream the matches produced by the query. The stream can not be consumed after the cursor is closed. The native + * nodes backing the matches are bound to the lifetime of the cursor. + * @return a stream of matches + */ public Stream stream() { return stream(null); } + /** + * Like {@link #stream()} but allows for custom predicates to be applied to the matches. + * @param predicate a function to handle custom predicates. + * @return a stream of matches + */ public Stream stream(@Nullable BiPredicate predicate) { return stream(arena , predicate); } + /** + * Like {@link #stream(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes. + * @param allocator allocator to use for allocating the native nodes backing the matches + * @param predicate a function to handle custom predicates. + * @return a stream of matches + */ public Stream stream(SegmentAllocator allocator, @Nullable BiPredicate predicate) { return StreamSupport.stream(new MatchesIterator(this, allocator, predicate), false); } + /** + * Get the next match produced by the query. The native nodes backing the match are bound to the lifetime of the cursor. + * @return the next match, if available + */ public Optional nextMatch() { return nextMatch(null); } + /** + * Like {@link #nextMatch()} but allows for custom predicates to be applied to the matches. + * @param predicate a function to handle custom predicates. + * @return the next match, if available + */ public Optional nextMatch(@Nullable BiPredicate predicate) { return nextMatch(arena, predicate); } + /** + * Like {@link #nextMatch(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes. + * @param allocator allocator to use for allocating the native nodes backing the matches + * @param predicate a function to handle custom predicates. + * @return the next match, if available + */ public Optional nextMatch(SegmentAllocator allocator, @Nullable BiPredicate predicate) { - var hasNoText = cursorRootNode.getTree().getText() == null; + var hasNoText = tree.getText() == null; MemorySegment match = arena.allocate(TSQueryMatch.layout()); while (ts_query_cursor_next_match(cursor, match)) { var count = Short.toUnsignedInt(TSQueryMatch.capture_count(match)); @@ -150,11 +180,13 @@ public Optional nextMatch(SegmentAllocator allocator, @Nullable BiPr var capture = TSQueryCapture.asSlice(matchCaptures, i); var name = query.getCaptureNames().get(TSQueryCapture.index(capture)); var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture)); - captureList.add(new QueryCapture(name, new Node(node, this.cursorRootNode.getTree()))); + captureList.add(new QueryCapture(name, new Node(node, tree))); } var patternIndex = TSQueryMatch.pattern_index(match); var matchId = TSQueryMatch.id(match); - var result = new QueryMatch(matchId, patternIndex, captureList); + // we copy all the data. So we can directly remove the match from the cursor to free memory. + ts_query_cursor_remove_match(cursor, matchId); + var result = new QueryMatch(patternIndex, captureList); if (hasNoText || matches(predicate, result)) { return Optional.of(result); } @@ -190,6 +222,11 @@ public MatchesIterator(QueryCursor cursor, SegmentAllocator allocator, @Nullable } @Override public boolean tryAdvance(Consumer action) { + + if(!cursor.arena.scope().isAlive()){ + throw new IllegalStateException("Cursor is closed. Cannot produce more matches."); + } + Optional queryMatch = cursor.nextMatch(allocator, predicate); queryMatch.ifPresent(action); return queryMatch.isPresent(); diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java b/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java index b77f1af..6ba288e 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java @@ -5,10 +5,10 @@ /** A match that corresponds to a certain pattern in the query. */ @NullMarked -public record QueryMatch(int matchId, @Unsigned int patternIndex, List captures) { +public record QueryMatch(@Unsigned int patternIndex, List captures) { /** Creates an instance of a QueryMatch record class. */ - public QueryMatch(@Unsigned int matchId, @Unsigned int patternIndex, List captures) { - this.matchId = matchId; + public QueryMatch(@Unsigned int patternIndex, List captures) { + this.patternIndex = patternIndex; this.captures = List.copyOf(captures); } @@ -24,6 +24,6 @@ public List findNodes(String capture) { @Override public String toString() { return String.format( - "QueryMatch[matchId=%s,patternIndex=%s, captures=%s]", Integer.toUnsignedString(matchId), Integer.toUnsignedString(patternIndex), captures); + "QueryMatch[patternIndex=%s, captures=%s]", Integer.toUnsignedString(patternIndex), captures); } } From 89dc1fcf2407e4219e1e6100195bfc816fbc09fe Mon Sep 17 00:00:00 2001 From: fbeutel Date: Fri, 10 Jan 2025 12:07:45 +0100 Subject: [PATCH 8/9] improved tests and documentation of QueryCursor --- .../github/treesitter/jtreesitter/Query.java | 65 +++-- .../treesitter/jtreesitter/QueryCursor.java | 81 +++--- .../jtreesitter/QueryCursorConfig.java | 124 +++++++++ .../jtreesitter/QueryCursorOptions.java | 71 ----- .../jtreesitter/QueryCursorTest.java | 251 ++++++++++++++++++ .../treesitter/jtreesitter/QueryTest.java | 5 - 6 files changed, 442 insertions(+), 155 deletions(-) create mode 100644 src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java delete mode 100644 src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java create mode 100644 src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java diff --git a/src/main/java/io/github/treesitter/jtreesitter/Query.java b/src/main/java/io/github/treesitter/jtreesitter/Query.java index 79bb6e3..8b81a5c 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/Query.java +++ b/src/main/java/io/github/treesitter/jtreesitter/Query.java @@ -12,7 +12,6 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Stream; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -25,7 +24,7 @@ @NullMarked public final class Query implements AutoCloseable { private final MemorySegment query; - private final QueryCursorOptions cursorOptions = new QueryCursorOptions(); + private final QueryCursorConfig cursorConfig = new QueryCursorConfig(); private final Arena arena; private final Language language; private final String source; @@ -261,7 +260,7 @@ private static boolean invalidPredicateChar(char c) { return !(Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == '?' || c == '!'); } - MemorySegment self(){ + MemorySegment self() { return query; } @@ -275,30 +274,30 @@ MemorySegment self(){ return ts_query_capture_count(query); } - public List> getPredicates(){ + public List> getPredicates() { return predicates.stream().map(Collections::unmodifiableList).toList(); } - public List getCaptureNames(){ + public List getCaptureNames() { return Collections.unmodifiableList(captureNames); } /** - * Get the maximum number of in-progress matches. + * Get the maximum number of in-progress matches of the default {@link QueryCursorConfig} * * @apiNote Defaults to {@code -1} (unlimited). */ public @Unsigned int getMatchLimit() { - return cursorOptions.getMatchLimit(); + return cursorConfig.getMatchLimit(); } /** - * Get the maximum number of in-progress matches. + * Set the maximum number of in-progress matches of the default {@link QueryCursorConfig} * * @throws IllegalArgumentException If {@code matchLimit == 0}. */ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentException { - cursorOptions.setMatchLimit(matchLimit); + cursorConfig.setMatchLimit(matchLimit); return this; } @@ -308,9 +307,11 @@ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentExcep * * @apiNote Defaults to {@code 0} (unlimited). * @since 0.23.1 + * @deprecated */ + @Deprecated(forRemoval = true) public @Unsigned long getTimeoutMicros() { - return cursorOptions.getTimeoutMicros(); + return cursorConfig.getTimeoutMicros(); } /** @@ -318,9 +319,11 @@ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentExcep * execution should be allowed to take before halting. * * @since 0.23.1 + * @deprecated */ + @Deprecated(forRemoval = true) public Query setTimeoutMicros(@Unsigned long timeoutMicros) { - cursorOptions.setTimeoutMicros(timeoutMicros); + cursorConfig.setTimeoutMicros(timeoutMicros); return this; } @@ -331,25 +334,22 @@ public Query setTimeoutMicros(@Unsigned long timeoutMicros) { *
Note that if a pattern includes many children, then they will still be checked. */ public Query setMaxStartDepth(@Unsigned int maxStartDepth) { - cursorOptions.setMaxStartDepth(maxStartDepth); + cursorConfig.setMaxStartDepth(maxStartDepth); return this; } /** Set the range of bytes in which the query will be executed. */ public Query setByteRange(@Unsigned int startByte, @Unsigned int endByte) { - cursorOptions.setStartByte(startByte); - cursorOptions.setEndByte(endByte); + cursorConfig.setByteRange(startByte, endByte); return this; } /** Set the range of points in which the query will be executed. */ public Query setPointRange(Point startPoint, Point endPoint) { - cursorOptions.setStartPoint(startPoint); - cursorOptions.setEndPoint(endPoint); + cursorConfig.setPointRange(startPoint, endPoint); return this; } - /** * Disable a certain pattern within a query. * @@ -477,14 +477,13 @@ public Map> getPatternAssertions(@Unsigned int index, b return Collections.unmodifiableMap(assertions.get(index)); } - /** - * Execute the query on a given node. + * Execute the query on a given node with the default {@link QueryCursorConfig}. * @param node The node that the query will run on. * @return A cursor that can be used to iterate over the matches. */ - public QueryCursor execute(Node node){ - return new QueryCursor(this, node, cursorOptions); + public QueryCursor execute(Node node) { + return new QueryCursor(this, node, cursorConfig); } /** @@ -493,22 +492,22 @@ public QueryCursor execute(Node node){ * @param options The options that will be used for this query. * @return A cursor that can be used to iterate over the matches. */ - public QueryCursor execute(Node node, QueryCursorOptions options){ + public QueryCursor execute(Node node, QueryCursorConfig options) { return new QueryCursor(this, node, options); } - /** * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned * matches is bound to the lifetime of this query object. * * @param node The node that the query will run on. + * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. + * For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#matchStream()}. */ public Stream findMatches(Node node) { return findMatches(node, arena, null); } - /** * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned * matches is bound to the lifetime of this query object. @@ -526,13 +525,13 @@ public Stream findMatches(Node node) { * * @param node The node that the query will run on. * @param predicate A function that handles custom predicates. - * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#stream(BiPredicate)}. + * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. + * For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#matchStream(BiPredicate)}. */ public Stream findMatches(Node node, @Nullable BiPredicate predicate) { return findMatches(node, arena, predicate); } - /** * Like {@link #findMatches(Node, BiPredicate)} but the native memory of the returned matches is created using the * given allocator. @@ -540,12 +539,15 @@ public Stream findMatches(Node node, @Nullable BiPredicate findMatches(Node node, SegmentAllocator allocator, @Nullable BiPredicate predicate) { - try(QueryCursor cursor = this.execute(node)){ + public Stream findMatches( + Node node, SegmentAllocator allocator, @Nullable BiPredicate predicate) { + try (QueryCursor cursor = this.execute(node)) { // make sure to load the entire stream into memory before closing the cursor. - // Otherwise, we call for nextMatch after closing the cursor which leads to undefined behavior. - return cursor.stream(allocator, predicate).toList().stream(); + // Otherwise, we call for nextMatch after closing the cursor which leads to an exception. + return cursor.matchStream(allocator, predicate).toList().stream(); } } @@ -559,13 +561,10 @@ public String toString() { return "Query{language=%s, source=%s}".formatted(language, source); } - private void checkIndex(@Unsigned int index) throws IndexOutOfBoundsException { if (Integer.compareUnsigned(index, getPatternCount()) >= 0) { throw new IndexOutOfBoundsException( "Pattern index %s is out of bounds".formatted(Integer.toUnsignedString(index))); } } - - } diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java index 9202fd5..04f9e68 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java @@ -1,11 +1,11 @@ package io.github.treesitter.jtreesitter; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; + import io.github.treesitter.jtreesitter.internal.TSNode; import io.github.treesitter.jtreesitter.internal.TSQueryCapture; import io.github.treesitter.jtreesitter.internal.TSQueryMatch; import io.github.treesitter.jtreesitter.internal.TreeSitter; -import org.jspecify.annotations.Nullable; - import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.SegmentAllocator; @@ -18,20 +18,16 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_did_exceed_match_limit; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_exec; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_match_limit; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_new; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_next_match; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_remove_match; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_byte_range; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_match_limit; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_max_start_depth; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_point_range; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_timeout_micros; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_timeout_micros; - -public class QueryCursor implements AutoCloseable{ +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Cursor for iterating over the matches produced by a {@link Query}. + *

+ * An instance of this class can be retrieved by calling {@link Query#execute(Node)}. + */ +@NullMarked +public class QueryCursor implements AutoCloseable { private final MemorySegment cursor; private final Arena arena; @@ -39,14 +35,13 @@ public class QueryCursor implements AutoCloseable{ private final Query query; private final Tree tree; - - QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorOptions options){ + QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorConfig config) { arena = Arena.ofConfined(); cursor = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete); this.query = query; this.tree = cursorRootNode.getTree(); - if(options != null){ - applyOptions(options); + if (config != null) { + applyConfig(config); } try (var alloc = Arena.ofConfined()) { @@ -54,7 +49,7 @@ public class QueryCursor implements AutoCloseable{ } } - private void applyOptions(QueryCursorOptions options){ + private void applyConfig(QueryCursorConfig options) { if (options.getStartByte() >= 0 && options.getEndByte() >= 0) { ts_query_cursor_set_byte_range(cursor, options.getStartByte(), options.getEndByte()); @@ -81,7 +76,6 @@ private void applyOptions(QueryCursorOptions options){ } } - /** * Get the maximum number of in-progress matches. * @@ -91,19 +85,19 @@ private void applyOptions(QueryCursorOptions options){ return ts_query_cursor_match_limit(cursor); } - /** * Get the maximum duration in microseconds that query * execution should be allowed to take before halting. * * @apiNote Defaults to {@code 0} (unlimited). * @since 0.23.1 + * @deprecated */ + @Deprecated(forRemoval = true) public @Unsigned long getTimeoutMicros() { return ts_query_cursor_timeout_micros(cursor); } - /** * Check if the query exceeded its maximum number of * in-progress matches during its last execution. @@ -112,39 +106,35 @@ public boolean didExceedMatchLimit() { return ts_query_cursor_did_exceed_match_limit(cursor); } - public void removeMatch(@Unsigned int matchId){ - ts_query_cursor_remove_match(cursor, matchId); - } - /** * Stream the matches produced by the query. The stream can not be consumed after the cursor is closed. The native * nodes backing the matches are bound to the lifetime of the cursor. * @return a stream of matches */ - public Stream stream() { - return stream(null); + public Stream matchStream() { + return matchStream(null); } /** - * Like {@link #stream()} but allows for custom predicates to be applied to the matches. + * Like {@link #matchStream()} but allows for custom predicates to be applied to the matches. * @param predicate a function to handle custom predicates. * @return a stream of matches */ - public Stream stream(@Nullable BiPredicate predicate) { - return stream(arena , predicate); + public Stream matchStream(@Nullable BiPredicate predicate) { + return matchStream(arena, predicate); } /** - * Like {@link #stream(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes. + * Like {@link #matchStream(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes. * @param allocator allocator to use for allocating the native nodes backing the matches * @param predicate a function to handle custom predicates. * @return a stream of matches */ - public Stream stream(SegmentAllocator allocator, @Nullable BiPredicate predicate) { + public Stream matchStream( + SegmentAllocator allocator, @Nullable BiPredicate predicate) { return StreamSupport.stream(new MatchesIterator(this, allocator, predicate), false); } - /** * Get the next match produced by the query. The native nodes backing the match are bound to the lifetime of the cursor. * @return the next match, if available @@ -168,7 +158,8 @@ public Optional nextMatch(@Nullable BiPredicate nextMatch(SegmentAllocator allocator, @Nullable BiPredicate predicate) { + public Optional nextMatch( + SegmentAllocator allocator, @Nullable BiPredicate predicate) { var hasNoText = tree.getText() == null; MemorySegment match = arena.allocate(TSQueryMatch.layout()); @@ -183,9 +174,6 @@ public Optional nextMatch(SegmentAllocator allocator, @Nullable BiPr captureList.add(new QueryCapture(name, new Node(node, tree))); } var patternIndex = TSQueryMatch.pattern_index(match); - var matchId = TSQueryMatch.id(match); - // we copy all the data. So we can directly remove the match from the cursor to free memory. - ts_query_cursor_remove_match(cursor, matchId); var result = new QueryMatch(patternIndex, captureList); if (hasNoText || matches(predicate, result)) { return Optional.of(result); @@ -201,7 +189,6 @@ private boolean matches(@Nullable BiPredicate predic }); } - @Override public void close() { arena.close(); @@ -214,24 +201,26 @@ private static final class MatchesIterator extends Spliterators.AbstractSplitera private final QueryCursor cursor; - public MatchesIterator(QueryCursor cursor, SegmentAllocator allocator, @Nullable BiPredicate predicate) { + public MatchesIterator( + QueryCursor cursor, + SegmentAllocator allocator, + @Nullable BiPredicate predicate) { super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); this.predicate = predicate; this.allocator = allocator; this.cursor = cursor; } + @Override public boolean tryAdvance(Consumer action) { - if(!cursor.arena.scope().isAlive()){ - throw new IllegalStateException("Cursor is closed. Cannot produce more matches."); + if (!cursor.arena.scope().isAlive()) { + throw new IllegalStateException("The underlying QueryCursor is closed. Cannot produce more matches."); } Optional queryMatch = cursor.nextMatch(allocator, predicate); queryMatch.ifPresent(action); return queryMatch.isPresent(); } - } - } diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java new file mode 100644 index 0000000..4d57bdc --- /dev/null +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java @@ -0,0 +1,124 @@ +package io.github.treesitter.jtreesitter; + +import org.jspecify.annotations.Nullable; + +/** + * Configuration for creating a {@link QueryCursor}. + * + * @see Query#execute(Node, QueryCursorConfig) + */ +public class QueryCursorConfig { + private int matchLimit = -1; // Default to unlimited + private long timeoutMicros = 0; // Default to unlimited + private int maxStartDepth = -1; + private int startByte = -1; + private int endByte = -1; + private Point startPoint; + private Point endPoint; + + /** + * Get the maximum number of in-progress matches. + * + * @apiNote Defaults to {@code -1} (unlimited). + */ + public int getMatchLimit() { + return matchLimit; + } + + /** + * Set the maximum number of in-progress matches. + * + * @apiNote Defaults to {@code -1} (unlimited). + */ + public void setMatchLimit(int matchLimit) throws IllegalArgumentException { + if (matchLimit == 0) { + throw new IllegalArgumentException("The match limit cannot equal 0"); + } + this.matchLimit = matchLimit; + } + + /** + * Get the maximum duration in microseconds that query + * execution should be allowed to take before halting. + * + * @return the timeout in microseconds + * @deprecated + */ + @Deprecated(forRemoval = true) + public long getTimeoutMicros() { + return timeoutMicros; + } + + /** + * Set the maximum duration in microseconds that query execution + * should be allowed to take before halting. + * + * @param timeoutMicros the timeout in microseconds + * @deprecated + */ + @Deprecated(forRemoval = true) + public void setTimeoutMicros(long timeoutMicros) { + this.timeoutMicros = timeoutMicros; + } + + /** + * Get the maximum start depth for the query cursor + */ + public int getMaxStartDepth() { + return maxStartDepth; + } + + /** + * Set the maximum start depth for the query cursor. + * + *

This prevents cursors from exploring children nodes at a certain depth. + *
Note that if a pattern includes many children, then they will still be checked. + */ + public void setMaxStartDepth(int maxStartDepth) { + this.maxStartDepth = maxStartDepth; + } + + /** + * Set the range of bytes in which the query will be executed. + */ + public void setByteRange(@Unsigned int startByte, @Unsigned int endByte) { + this.startByte = startByte; + this.endByte = endByte; + } + + /** + * Get the start byte of the range of bytes in which the query will be executed or -1 if not set. + */ + public int getStartByte() { + return startByte; + } + + /** + * Get the end byte of the range of bytes in which the query will be executed or -1 if not set. + */ + public int getEndByte() { + return endByte; + } + + /** + * Set the range of points in which the query will be executed. + */ + public void setPointRange(Point startPoint, Point endPoint) { + this.startPoint = startPoint; + this.endPoint = endPoint; + } + + /** + * Get the start point of the range of points in which the query will be executed or {@code null} if not set. + */ + public @Nullable Point getStartPoint() { + return startPoint; + } + + /** + * Get the end point of the range of points in which the query will be executed or {@code null} if not set. + */ + public @Nullable Point getEndPoint() { + return endPoint; + } +} diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java deleted file mode 100644 index 36941bf..0000000 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.github.treesitter.jtreesitter; - -public class QueryCursorOptions { - private int matchLimit = -1; // Default to unlimited - private long timeoutMicros = -1; // Default to unlimited - private int maxStartDepth = -1; - private int startByte = -1; - private int endByte = -1; - private Point startPoint; - private Point endPoint; - - - public int getMatchLimit() { - return matchLimit; - } - - public void setMatchLimit(int matchLimit) throws IllegalArgumentException { - if (matchLimit == 0) { - throw new IllegalArgumentException("The match limit cannot equal 0"); - } - this.matchLimit = matchLimit; - } - - public long getTimeoutMicros() { - return timeoutMicros; - } - - public void setTimeoutMicros(long timeoutMicros) { - this.timeoutMicros = timeoutMicros; - } - - public int getMaxStartDepth() { - return maxStartDepth; - } - - public void setMaxStartDepth(int maxStartDepth) { - this.maxStartDepth = maxStartDepth; - } - - public int getStartByte() { - return startByte; - } - - public void setStartByte(int startByte) { - this.startByte = startByte; - } - - public int getEndByte() { - return endByte; - } - - public void setEndByte(int endByte) { - this.endByte = endByte; - } - - public Point getStartPoint() { - return startPoint; - } - - public void setStartPoint(Point startPoint) { - this.startPoint = startPoint; - } - - public Point getEndPoint() { - return endPoint; - } - - public void setEndPoint(Point endPoint) { - this.endPoint = endPoint; - } -} diff --git a/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java b/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java new file mode 100644 index 0000000..418ac9b --- /dev/null +++ b/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java @@ -0,0 +1,251 @@ +package io.github.treesitter.jtreesitter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import io.github.treesitter.jtreesitter.languages.TreeSitterJava; +import java.lang.foreign.Arena; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class QueryCursorTest { + + private static Language language; + private static Parser parser; + + @BeforeAll + static void beforeAll() { + language = new Language(TreeSitterJava.language()); + parser = new Parser(language); + } + + @AfterAll + static void afterAll() { + parser.close(); + } + + private static void assertQueryCursor(String querySource, String source, Consumer assertions) { + assertQueryCursor(querySource, source, null, assertions); + } + + private static void assertQueryCursor( + String querySource, String source, QueryCursorConfig config, Consumer assertions) { + try (var tree = parser.parse(source).orElseThrow(); + Query query = new Query(language, querySource)) { + assertQueryCursor(query, tree.getRootNode(), config, assertions); + } catch (QueryError e) { + fail("Unexpected query error", e); + } + } + + private static void assertQueryCursor( + Query query, Node node, QueryCursorConfig config, Consumer assertions) { + try (var queryCursor = query.execute(node, config)) { + assertions.accept(queryCursor); + } + } + + @Test + void nextMatch() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + """; + + assertQueryCursor(querySource, source, qc -> { + Optional currentMatch = qc.nextMatch(); + + assertTrue(currentMatch.isPresent()); + assertEquals("a", currentMatch.get().captures().getFirst().node().getText()); + + currentMatch = qc.nextMatch(); + assertTrue(currentMatch.isPresent()); + assertEquals("b", currentMatch.get().captures().getFirst().node().getText()); + + currentMatch = qc.nextMatch(); + assertFalse(currentMatch.isPresent()); + }); + } + + @Test + void matchStream() { + + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + assertQueryCursor(querySource, source, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(3, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("a", "b", "c"), texts); + }); + } + + @Test + void didExceedMatchLimit() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + assertQueryCursor(querySource, source, qc -> { + assertFalse(qc.didExceedMatchLimit()); + }); + } + + @Test + void testByteRange() { + + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + QueryCursorConfig config = new QueryCursorConfig(); + config.setByteRange(6, 20); // should ignore a + + assertQueryCursor(querySource, source, config, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(2, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("b", "c"), texts); + }); + } + + @Test + void testPointRange() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + QueryCursorConfig config = new QueryCursorConfig(); + config.setPointRange(new Point(0, 0), new Point(1, 0)); // should ignore c + + assertQueryCursor(querySource, source, config, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(2, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("a", "b"), texts); + }); + } + + @Test + void testStartDepth() { + + String queryString = "(local_variable_declaration) @decl"; + + String source = + """ + int a = b; + void foo() { + int c = 3; + } + """; + + QueryCursorConfig config = new QueryCursorConfig(); + config.setMaxStartDepth(1); // should ignore second declaration as it is nested in method body + + assertQueryCursor(queryString, source, config, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(1, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("int a = b;"), texts); + }); + } + + @Test + void testCustomAllocator() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + try (Arena arena = Arena.ofConfined()) { + + List matches = new ArrayList<>(); + + try (var tree = parser.parse(source).orElseThrow(); + Query query = new Query(language, querySource)) { + + try (var queryCursor = query.execute(tree.getRootNode())) { + List queryMatches = + queryCursor.matchStream(arena, null).toList(); + matches.addAll(queryMatches); + } + + // check that we can still work with the nodes after closing the cursor + List parentTexts = matches.stream() + .flatMap(qm -> qm.captures().stream()) + .flatMap(cap -> cap.node().getParent().stream()) + .map(Node::getText) + .toList(); + + assertEquals(List.of("a = b", "a = b", "c = 3"), parentTexts); + } catch (QueryError e) { + fail("Unexpected query error", e); + } + } + } + + @Test + void testUseMatchStreamAfterClose() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + try (var tree = parser.parse(source).orElseThrow(); + Query query = new Query(language, querySource)) { + + Stream queryStream = null; + try (var queryCursor = query.execute(tree.getRootNode())) { + queryStream = queryCursor.matchStream(); + } + // we cannot use the stream after closing the cursor + assertThrows(IllegalStateException.class, queryStream::toList); + } catch (QueryError e) { + fail("Unexpected query error", e); + } + } +} diff --git a/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java b/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java index f5a8bc0..6b4de54 100644 --- a/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java +++ b/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java @@ -132,11 +132,6 @@ void setPointRange() { }); } - @Test - void didExceedMatchLimit() { - assertQuery(query -> assertFalse(query.didExceedMatchLimit())); - } - @Test void disablePattern() { assertQuery(query -> { From 6262cfad53ddcf578188c993391ee3ce46fec055 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Fri, 10 Jan 2025 14:43:34 +0100 Subject: [PATCH 9/9] apply formatting --- .../java/io/github/treesitter/jtreesitter/QueryCursor.java | 1 - src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java | 1 - .../java/io/github/treesitter/jtreesitter/TreeCursorTest.java | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java index 04f9e68..2714b20 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java @@ -17,7 +17,6 @@ import java.util.function.Consumer; import java.util.stream.Stream; import java.util.stream.StreamSupport; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; diff --git a/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java b/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java index 08feea1..0a38cd3 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java @@ -50,7 +50,6 @@ public Node getCurrentNode() { return this.node; } - /** * Get the current node of the cursor. Its native memory will be managed by the given allocator. * @param allocator the allocator to use for managing the native memory of the node diff --git a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java index f49f978..b05577f 100644 --- a/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java +++ b/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java @@ -3,9 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import io.github.treesitter.jtreesitter.languages.TreeSitterJava; -import org.junit.jupiter.api.*; - import java.lang.foreign.Arena; +import org.junit.jupiter.api.*; class TreeCursorTest { private static Tree tree;