> action) {
41 | var hasNoText = tree.getText() == null;
42 | MemorySegment match = allocator.allocate(TSQueryMatch.layout());
43 | MemorySegment index = allocator.allocate(C_INT);
44 | var captureNames = query.getCaptureNames();
45 | while (ts_query_cursor_next_capture(cursor, match, index)) {
46 | var result = QueryMatch.from(match, captureNames, tree, allocator);
47 | if (hasNoText || query.matches(predicate, result)) {
48 | var entry = new SimpleImmutableEntry<>(index.get(C_INT, 0), result);
49 | action.accept(entry);
50 | return true;
51 | }
52 | }
53 | return false;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/InputEdit.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import io.github.treesitter.jtreesitter.internal.TSInputEdit;
4 | import java.lang.foreign.MemorySegment;
5 | import java.lang.foreign.SegmentAllocator;
6 | import org.jspecify.annotations.NullMarked;
7 |
8 | /** An edit to a text document. */
9 | @NullMarked
10 | public record InputEdit(
11 | @Unsigned int startByte,
12 | @Unsigned int oldEndByte,
13 | @Unsigned int newEndByte,
14 | Point startPoint,
15 | Point oldEndPoint,
16 | Point newEndPoint) {
17 |
18 | MemorySegment into(SegmentAllocator allocator) {
19 | var inputEdit = TSInputEdit.allocate(allocator);
20 | TSInputEdit.start_byte(inputEdit, startByte);
21 | TSInputEdit.old_end_byte(inputEdit, oldEndByte);
22 | TSInputEdit.new_end_byte(inputEdit, newEndByte);
23 | TSInputEdit.start_point(inputEdit, startPoint.into(allocator));
24 | TSInputEdit.old_end_point(inputEdit, oldEndPoint.into(allocator));
25 | TSInputEdit.new_end_point(inputEdit, newEndPoint.into(allocator));
26 | return inputEdit;
27 | }
28 |
29 | @Override
30 | public String toString() {
31 | return String.format(
32 | "InputEdit[startByte=%s, oldEndByte=%s, newEndByte=%s, "
33 | + "startPoint=%s, oldEndPoint=%s, newEndPoint=%s]",
34 | Integer.toUnsignedString(startByte),
35 | Integer.toUnsignedString(oldEndByte),
36 | Integer.toUnsignedString(newEndByte),
37 | startPoint,
38 | oldEndPoint,
39 | newEndPoint);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/InputEncoding.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import java.nio.ByteOrder;
4 | import java.nio.charset.Charset;
5 | import java.nio.charset.StandardCharsets;
6 | import org.jspecify.annotations.NonNull;
7 |
8 | /** The encoding of source code. */
9 | public enum InputEncoding {
10 | /** UTF-8 encoding. */
11 | UTF_8(StandardCharsets.UTF_8),
12 | /**
13 | * UTF-16 little endian encoding.
14 | *
15 | * @since 0.25.0
16 | */
17 | UTF_16LE(StandardCharsets.UTF_16LE),
18 | /**
19 | * UTF-16 big endian encoding.
20 | *
21 | * @since 0.25.0
22 | */
23 | UTF_16BE(StandardCharsets.UTF_16BE);
24 |
25 | private final @NonNull Charset charset;
26 |
27 | InputEncoding(@NonNull Charset charset) {
28 | this.charset = charset;
29 | }
30 |
31 | Charset charset() {
32 | return charset;
33 | }
34 |
35 | private static final boolean IS_BIG_ENDIAN = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN);
36 |
37 | /**
38 | * Convert a standard {@linkplain Charset} to an {@linkplain InputEncoding}.
39 | *
40 | * @param charset one of {@link StandardCharsets#UTF_8}, {@link StandardCharsets#UTF_16BE},
41 | * {@link StandardCharsets#UTF_16LE}, or {@link StandardCharsets#UTF_16} (native byte order).
42 | * @throws IllegalArgumentException If the character set is invalid.
43 | */
44 | @SuppressWarnings("SameParameterValue")
45 | public static @NonNull InputEncoding valueOf(@NonNull Charset charset) throws IllegalArgumentException {
46 | if (charset.equals(StandardCharsets.UTF_8)) return InputEncoding.UTF_8;
47 | if (charset.equals(StandardCharsets.UTF_16BE)) return InputEncoding.UTF_16BE;
48 | if (charset.equals(StandardCharsets.UTF_16LE)) return InputEncoding.UTF_16LE;
49 | if (charset.equals(StandardCharsets.UTF_16)) {
50 | return IS_BIG_ENDIAN ? InputEncoding.UTF_16BE : InputEncoding.UTF_16LE;
51 | }
52 | throw new IllegalArgumentException("Invalid character set: %s".formatted(charset));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Language.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import io.github.treesitter.jtreesitter.internal.TSLanguageMetadata;
6 | import java.lang.foreign.*;
7 | import org.jspecify.annotations.NullMarked;
8 | import org.jspecify.annotations.Nullable;
9 |
10 | /** A class that defines how to parse a particular language. */
11 | @NullMarked
12 | public final class Language implements Cloneable {
13 | /**
14 | * The latest ABI version that is supported by the current version of the library.
15 | *
16 | * @apiNote The Tree-sitter library is generally backwards-compatible with
17 | * languages generated using older CLI versions, but is not forwards-compatible.
18 | */
19 | public static final @Unsigned int LANGUAGE_VERSION = TREE_SITTER_LANGUAGE_VERSION();
20 |
21 | /** The earliest ABI version that is supported by the current version of the library. */
22 | public static final @Unsigned int MIN_COMPATIBLE_LANGUAGE_VERSION = TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION();
23 |
24 | private static final ValueLayout VOID_PTR =
25 | ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_BYTE));
26 |
27 | private static final FunctionDescriptor FUNC_DESC = FunctionDescriptor.of(VOID_PTR);
28 |
29 | private static final Linker LINKER = Linker.nativeLinker();
30 |
31 | private final MemorySegment self;
32 |
33 | private final @Unsigned int version;
34 |
35 | /**
36 | * Creates a new instance from the given language pointer.
37 | *
38 | * @implNote It is up to the caller to ensure that the pointer is valid.
39 | *
40 | * @throws IllegalArgumentException If the language version is incompatible.
41 | */
42 | public Language(MemorySegment self) throws IllegalArgumentException {
43 | this.self = self.asReadOnly();
44 | version = ts_language_abi_version(this.self);
45 | if (version < MIN_COMPATIBLE_LANGUAGE_VERSION || version > LANGUAGE_VERSION) {
46 | throw new IllegalArgumentException(String.format(
47 | "Incompatible language version %d. Must be between %d and %d.",
48 | version, MIN_COMPATIBLE_LANGUAGE_VERSION, LANGUAGE_VERSION));
49 | }
50 | }
51 |
52 | private static UnsatisfiedLinkError unresolved(String name) {
53 | return new UnsatisfiedLinkError("Unresolved symbol: %s".formatted(name));
54 | }
55 |
56 | /**
57 | * Load a language by looking for its function in the given symbols.
58 | *
59 | * Example
60 | *
61 | * {@snippet lang="java" :
62 | * String library = System.mapLibraryName("tree-sitter-java");
63 | * SymbolLookup symbols = SymbolLookup.libraryLookup(library, Arena.global());
64 | * Language language = Language.load(symbols, "tree_sitter_java");
65 | * }
66 | *
67 | * The {@linkplain Arena} used to load the language
68 | * must not be closed while the language is being used.
69 | *
70 | * @throws RuntimeException If the language could not be loaded.
71 | * @since 0.23.1
72 | */
73 | // TODO: deprecate when the bindings are generated by the CLI
74 | public static Language load(SymbolLookup symbols, String language) throws RuntimeException {
75 | var address = symbols.find(language).orElseThrow(() -> unresolved(language));
76 | try {
77 | var function = LINKER.downcallHandle(address, FUNC_DESC);
78 | return new Language((MemorySegment) function.invokeExact());
79 | } catch (Throwable e) {
80 | throw new RuntimeException("Failed to load %s".formatted(language), e);
81 | }
82 | }
83 |
84 | MemorySegment segment() {
85 | return self;
86 | }
87 |
88 | /**
89 | * Get the ABI version number for this language.
90 | *
91 | *
This version number is used to ensure that languages
92 | * were generated by a compatible version of Tree-sitter.
93 | *
94 | * @since 0.25.0
95 | */
96 | public @Unsigned int getAbiVersion() {
97 | return version;
98 | }
99 |
100 | /**
101 | * Get the ABI version number for this language.
102 | *
103 | * @deprecated Use {@link #getAbiVersion} instead.
104 | */
105 | @Deprecated(since = "0.25.0", forRemoval = true)
106 | public @Unsigned int getVersion() {
107 | return version;
108 | }
109 |
110 | /** Get the name of this language, if available. */
111 | public @Nullable String getName() {
112 | var name = ts_language_name(self);
113 | return name.equals(MemorySegment.NULL) ? null : name.getString(0);
114 | }
115 |
116 | /**
117 | * Get the metadata for this language, if available.
118 | *
119 | * @apiNote This information is generated by the Tree-sitter
120 | * CLI and relies on the language author providing the correct
121 | * metadata in the language's {@code tree-sitter.json} file.
122 | *
123 | * @since 0.25.0
124 | */
125 | public @Nullable LanguageMetadata getMetadata() {
126 | var metadata = ts_language_metadata(self);
127 | if (metadata.equals(MemorySegment.NULL)) return null;
128 |
129 | short major = TSLanguageMetadata.major_version(metadata);
130 | short minor = TSLanguageMetadata.minor_version(metadata);
131 | short patch = TSLanguageMetadata.patch_version(metadata);
132 | var version = new LanguageMetadata.Version(major, minor, patch);
133 | return new LanguageMetadata(version);
134 | }
135 |
136 | /** Get the number of distinct node types in this language. */
137 | public @Unsigned int getSymbolCount() {
138 | return ts_language_symbol_count(self);
139 | }
140 |
141 | /** Get the number of valid states in this language */
142 | public @Unsigned int getStateCount() {
143 | return ts_language_state_count(self);
144 | }
145 |
146 | /** Get the number of distinct field names in this language */
147 | public @Unsigned int getFieldCount() {
148 | return ts_language_field_count(self);
149 | }
150 |
151 | /**
152 | * Get all supertype symbols for the language.
153 | *
154 | * @since 0.25.0
155 | */
156 | public @Unsigned short[] getSupertypes() {
157 | try (var alloc = Arena.ofConfined()) {
158 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment());
159 | var supertypes = ts_language_supertypes(self, length);
160 | var isEmpty = length.get(C_INT, 0) == 0;
161 | return isEmpty ? new short[0] : supertypes.toArray(C_SHORT);
162 | }
163 | }
164 |
165 | /**
166 | * Get all symbols for a given supertype symbol.
167 | *
168 | * @since 0.25.0
169 | * @see #getSupertypes()
170 | */
171 | public @Unsigned short[] getSubtypes(@Unsigned short supertype) {
172 | try (var alloc = Arena.ofConfined()) {
173 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment());
174 | var subtypes = ts_language_subtypes(self, supertype, length);
175 | var isEmpty = length.get(C_INT, 0) == 0;
176 | return isEmpty ? new short[0] : subtypes.toArray(C_SHORT);
177 | }
178 | }
179 |
180 | /** Get the node type for the given numerical ID. */
181 | public @Nullable String getSymbolName(@Unsigned short symbol) {
182 | var name = ts_language_symbol_name(self, symbol);
183 | return name.equals(MemorySegment.NULL) ? null : name.getString(0);
184 | }
185 |
186 | /** Get the numerical ID for the given node type, or {@code 0} if not found. */
187 | public @Unsigned short getSymbolForName(String name, boolean isNamed) {
188 | try (var arena = Arena.ofConfined()) {
189 | var segment = arena.allocateFrom(name);
190 | return ts_language_symbol_for_name(self, segment, name.length(), isNamed);
191 | }
192 | }
193 |
194 | /**
195 | * Check if the node for the given numerical ID is named.
196 | *
197 | * @see Node#isNamed
198 | */
199 | public boolean isNamed(@Unsigned short symbol) {
200 | return ts_language_symbol_type(self, symbol) == TSSymbolTypeRegular();
201 | }
202 |
203 | /** Check if the node for the given numerical ID is visible. */
204 | public boolean isVisible(@Unsigned short symbol) {
205 | return ts_language_symbol_type(self, symbol) <= TSSymbolTypeAnonymous();
206 | }
207 |
208 | /**
209 | * Check if the node for the given numerical ID is a supertype.
210 | *
211 | * @since 0.24.0
212 | */
213 | public boolean isSupertype(@Unsigned short symbol) {
214 | return ts_language_symbol_type(self, symbol) == TSSymbolTypeSupertype();
215 | }
216 |
217 | /** Get the field name for the given numerical id. */
218 | public @Nullable String getFieldNameForId(@Unsigned short id) {
219 | var name = ts_language_field_name_for_id(self, id);
220 | return name.equals(MemorySegment.NULL) ? null : name.getString(0);
221 | }
222 |
223 | /** Get the numerical ID for the given field name. */
224 | public @Unsigned short getFieldIdForName(String name) {
225 | try (var arena = Arena.ofConfined()) {
226 | var segment = arena.allocateFrom(name);
227 | return ts_language_field_id_for_name(self, segment, name.length());
228 | }
229 | }
230 |
231 | /**
232 | * Get the next parse state.
233 | *
234 | *
{@snippet lang="java" :
235 | * short state = language.nextState(node.getParseState(), node.getGrammarSymbol());
236 | * }
237 | *
238 | *
Combine this with {@link #lookaheadIterator lookaheadIterator(state)}
239 | * to generate completion suggestions or valid symbols in {@index ERROR} nodes.
240 | */
241 | public @Unsigned short nextState(@Unsigned short state, @Unsigned short symbol) {
242 | return ts_language_next_state(self, state, symbol);
243 | }
244 |
245 | /**
246 | * Create a new lookahead iterator for the given parse state.
247 | *
248 | * @throws IllegalArgumentException If the state is invalid for this language.
249 | */
250 | public LookaheadIterator lookaheadIterator(@Unsigned short state) throws IllegalArgumentException {
251 | return new LookaheadIterator(self, state);
252 | }
253 |
254 | /**
255 | * Create a new query from a string containing one or more S-expression patterns.
256 | *
257 | * @throws QueryError If an error occurred while creating the query.
258 | * @deprecated Use the {@link Query#Query(Language, String) Query} constructor instead.
259 | */
260 | @Deprecated(since = "0.25.0")
261 | public Query query(String source) throws QueryError {
262 | return new Query(this, source);
263 | }
264 |
265 | /**
266 | * Get another reference to the language.
267 | *
268 | * @since 0.24.0
269 | */
270 | @Override
271 | @SuppressWarnings("MethodDoesntCallSuperMethod")
272 | public Language clone() {
273 | return new Language(ts_language_copy(self));
274 | }
275 |
276 | @Override
277 | public boolean equals(Object o) {
278 | if (this == o) return true;
279 | if (!(o instanceof Language other)) return false;
280 | return self.equals(other.self);
281 | }
282 |
283 | @Override
284 | public int hashCode() {
285 | return Long.hashCode(self.address());
286 | }
287 |
288 | @Override
289 | public String toString() {
290 | return "Language{id=0x%x, version=%d}".formatted(self.address(), version);
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/LanguageMetadata.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import org.jspecify.annotations.NullMarked;
4 |
5 | /**
6 | * The metadata associated with a {@linkplain Language}.
7 | *
8 | * @since 0.25.0
9 | */
10 | @NullMarked
11 | public record LanguageMetadata(Version version) {
12 | /**
13 | * The Semantic Version of the {@linkplain Language}.
14 | *
15 | *
This version information may be used to signal if a given parser
16 | * is incompatible with existing queries when upgrading between versions.
17 | *
18 | * @since 0.25.0
19 | */
20 | public record Version(@Unsigned short major, @Unsigned short minor, @Unsigned short patch) {
21 | @Override
22 | public String toString() {
23 | return "%d.%d.%d".formatted(major, minor, patch);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Logger.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import java.util.function.BiConsumer;
4 | import org.jspecify.annotations.NonNull;
5 |
6 | /** A function that logs parsing results. */
7 | @FunctionalInterface
8 | public interface Logger extends BiConsumer {
9 | /**
10 | * {@inheritDoc}
11 | *
12 | * @param type the log type
13 | * @param message the log message
14 | */
15 | @Override
16 | void accept(@NonNull Type type, @NonNull String message);
17 |
18 | /** The type of a log message. */
19 | enum Type {
20 | /** Lexer message. */
21 | LEX,
22 | /** Parser message. */
23 | PARSE
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/LookaheadIterator.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import io.github.treesitter.jtreesitter.internal.TreeSitter;
6 | import java.lang.foreign.Arena;
7 | import java.lang.foreign.MemorySegment;
8 | import java.util.*;
9 | import java.util.function.Consumer;
10 | import java.util.stream.Stream;
11 | import java.util.stream.StreamSupport;
12 | import org.jspecify.annotations.NullMarked;
13 |
14 | /**
15 | * A class that is used to look up valid symbols in a specific parse state.
16 | *
17 | * Lookahead iterators can be useful to generate suggestions and improve syntax error diagnostics.
18 | * To get symbols valid in an {@index ERROR} node, use the lookahead iterator on its first leaf node state.
19 | * For {@index MISSING} nodes, a lookahead iterator created on the previous non-extra leaf node may be appropriate.
20 | */
21 | @NullMarked
22 | public final class LookaheadIterator implements AutoCloseable, Iterator {
23 | private final Arena arena;
24 | private final MemorySegment self;
25 | private final short state;
26 | private boolean iterFirst = true;
27 | private boolean hasNext = false;
28 |
29 | LookaheadIterator(MemorySegment language, @Unsigned short state) throws IllegalArgumentException {
30 | var iterator = ts_lookahead_iterator_new(language, state);
31 | if (iterator == null) {
32 | throw new IllegalArgumentException(
33 | "State %d is not valid for %s".formatted(Short.toUnsignedInt(state), this));
34 | }
35 | this.state = state;
36 | arena = Arena.ofShared();
37 | self = iterator.reinterpret(arena, TreeSitter::ts_lookahead_iterator_delete);
38 | }
39 |
40 | /** Get the current language of the lookahead iterator. */
41 | public Language getLanguage() {
42 | return new Language(ts_lookahead_iterator_language(self));
43 | }
44 |
45 | /**
46 | * Get the current symbol ID.
47 | *
48 | * @apiNote The ID of the {@index ERROR} symbol is equal to {@code -1}.
49 | */
50 | public @Unsigned short getCurrentSymbol() {
51 | return ts_lookahead_iterator_current_symbol(self);
52 | }
53 |
54 | /**
55 | * The current symbol name.
56 | *
57 | * @apiNote Newly created lookahead iterators will contain the {@index ERROR} symbol.
58 | */
59 | public String getCurrentSymbolName() {
60 | return ts_lookahead_iterator_current_symbol_name(self).getString(0);
61 | }
62 |
63 | /**
64 | * Reset the lookahead iterator to the given state.
65 | *
66 | * @return {@code true} if the iterator was reset
67 | * successfully or {@code false} if it failed.
68 | */
69 | public boolean reset(@Unsigned short state) {
70 | return ts_lookahead_iterator_reset_state(self, state);
71 | }
72 |
73 | /**
74 | * Reset the lookahead iterator to the given state and another language.
75 | *
76 | * @return {@code true} if the iterator was reset
77 | * successfully or {@code false} if it failed.
78 | */
79 | public boolean reset(@Unsigned short state, Language language) {
80 | return ts_lookahead_iterator_reset(self, language.segment(), state);
81 | }
82 |
83 | /** Check if the lookahead iterator has more symbols. */
84 | @Override
85 | public boolean hasNext() {
86 | if (iterFirst) {
87 | iterFirst = false;
88 | hasNext = ts_lookahead_iterator_next(self);
89 | ts_lookahead_iterator_reset_state(self, state);
90 | }
91 | return hasNext;
92 | }
93 |
94 | /**
95 | * Advance the lookahead iterator to the next symbol.
96 | *
97 | * @throws NoSuchElementException If there are no more symbols.
98 | */
99 | @Override
100 | public Symbol next() throws NoSuchElementException {
101 | if (!hasNext()) throw new NoSuchElementException();
102 | hasNext = ts_lookahead_iterator_next(self);
103 | return new Symbol(getCurrentSymbol(), getCurrentSymbolName());
104 | }
105 |
106 | /**
107 | * Iterate over the symbol IDs.
108 | *
109 | * @implNote Calling this method will reset the iterator to its original state.
110 | */
111 | public @Unsigned Stream symbols() {
112 | ts_lookahead_iterator_reset_state(self, state);
113 | return StreamSupport.stream(new IdIterator(self), false);
114 | }
115 |
116 | /**
117 | * Iterate over the symbol names.
118 | *
119 | * @implNote Calling this method will reset the iterator to its original state.
120 | */
121 | public Stream names() {
122 | ts_lookahead_iterator_reset_state(self, state);
123 | return StreamSupport.stream(new NameIterator(self), false);
124 | }
125 |
126 | @Override
127 | public void close() throws RuntimeException {
128 | arena.close();
129 | }
130 |
131 | /** @hidden */
132 | @Override
133 | public void remove() {
134 | Iterator.super.remove();
135 | }
136 |
137 | /** A class that pairs a symbol ID with its name. */
138 | public record Symbol(@Unsigned short id, String name) {}
139 |
140 | private static final class IdIterator extends Spliterators.AbstractSpliterator {
141 | private final MemorySegment iterator;
142 |
143 | private IdIterator(MemorySegment iterator) {
144 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.SORTED | Spliterator.ORDERED);
145 | this.iterator = iterator;
146 | }
147 |
148 | @Override
149 | public Comparator super Short> getComparator() {
150 | return Short::compareUnsigned;
151 | }
152 |
153 | @Override
154 | public boolean tryAdvance(Consumer super Short> action) {
155 | var result = ts_lookahead_iterator_next(iterator);
156 | if (result) {
157 | var symbol = ts_lookahead_iterator_current_symbol(iterator);
158 | action.accept(symbol);
159 | }
160 | return result;
161 | }
162 | }
163 |
164 | private static final class NameIterator extends Spliterators.AbstractSpliterator {
165 | private final MemorySegment iterator;
166 |
167 | private NameIterator(MemorySegment iterator) {
168 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL);
169 | this.iterator = iterator;
170 | }
171 |
172 | @Override
173 | public boolean tryAdvance(Consumer super String> action) {
174 | var result = ts_lookahead_iterator_next(iterator);
175 | if (result) {
176 | var name = ts_lookahead_iterator_current_symbol_name(iterator);
177 | action.accept(name.getString(0));
178 | }
179 | return result;
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/MatchesIterator.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_next_match;
4 |
5 | import io.github.treesitter.jtreesitter.internal.TSQueryMatch;
6 | import java.lang.foreign.MemorySegment;
7 | import java.lang.foreign.SegmentAllocator;
8 | import java.util.Spliterator;
9 | import java.util.Spliterators;
10 | import java.util.function.BiPredicate;
11 | import java.util.function.Consumer;
12 | import org.jspecify.annotations.NullMarked;
13 | import org.jspecify.annotations.Nullable;
14 |
15 | @NullMarked
16 | class MatchesIterator extends Spliterators.AbstractSpliterator {
17 | private final @Nullable BiPredicate predicate;
18 | private final Tree tree;
19 | private final SegmentAllocator allocator;
20 | private final Query query;
21 | private final MemorySegment cursor;
22 |
23 | public MatchesIterator(
24 | Query query,
25 | MemorySegment cursor,
26 | Tree tree,
27 | SegmentAllocator allocator,
28 | @Nullable BiPredicate predicate) {
29 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL);
30 | this.predicate = predicate;
31 | this.tree = tree;
32 | this.allocator = allocator;
33 | this.query = query;
34 | this.cursor = cursor;
35 | }
36 |
37 | @Override
38 | public boolean tryAdvance(Consumer super QueryMatch> action) {
39 | var hasNoText = tree.getText() == null;
40 | MemorySegment match = allocator.allocate(TSQueryMatch.layout());
41 | var captureNames = query.getCaptureNames();
42 | while (ts_query_cursor_next_match(cursor, match)) {
43 | var result = QueryMatch.from(match, captureNames, tree, allocator);
44 | if (hasNoText || query.matches(predicate, result)) {
45 | action.accept(result);
46 | return true;
47 | }
48 | }
49 | return false;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/NativeLibraryLookup.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import java.lang.foreign.Arena;
4 | import java.lang.foreign.SymbolLookup;
5 |
6 | /**
7 | * An interface implemented by clients that wish to customize the {@link SymbolLookup}
8 | * used for the tree-sitter native library. Implementations must be registered
9 | * by listing their fully qualified class name in a resource file named
10 | * {@code META-INF/services/io.github.treesitter.jtreesitter.NativeLibraryLookup}.
11 | *
12 | * @since 0.25.0
13 | * @see java.util.ServiceLoader
14 | */
15 | @FunctionalInterface
16 | public interface NativeLibraryLookup {
17 | /**
18 | * Get the {@link SymbolLookup} to be used for the tree-sitter native library.
19 | *
20 | * @param arena The arena that will manage the native memory.
21 | * @since 0.25.0
22 | */
23 | SymbolLookup get(Arena arena);
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Node.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import io.github.treesitter.jtreesitter.internal.TSNode;
6 | import java.lang.foreign.Arena;
7 | import java.lang.foreign.MemorySegment;
8 | import java.util.*;
9 | import org.jspecify.annotations.NullMarked;
10 | import org.jspecify.annotations.Nullable;
11 |
12 | /**
13 | * A single node within a {@linkplain Tree syntax tree}.
14 | *
15 | * @implNote Node lifetimes are tied to the {@link Tree},
16 | * {@link TreeCursor}, or {@link Query} that they belong to.
17 | */
18 | @NullMarked
19 | public final class Node {
20 | private final MemorySegment self;
21 | private final Tree tree;
22 | private @Nullable List children;
23 | private final Arena arena = Arena.ofAuto();
24 | private boolean wasEdited = false;
25 |
26 | Node(MemorySegment self, Tree tree) {
27 | this.self = self;
28 | this.tree = tree;
29 | }
30 |
31 | private Optional optional(MemorySegment node) {
32 | return ts_node_is_null(node) ? Optional.empty() : Optional.of(new Node(node, tree));
33 | }
34 |
35 | MemorySegment copy(Arena arena) {
36 | return self.reinterpret(arena, null);
37 | }
38 |
39 | /** Get the tree that contains this node. */
40 | public Tree getTree() {
41 | return tree;
42 | }
43 |
44 | /**
45 | * Get the numerical ID of the node.
46 | *
47 | * @apiNote Within any given syntax tree, no two nodes have the same ID. However,
48 | * if a new tree is created based on an older tree, and a node from the old tree
49 | * is reused in the process, then that node will have the same ID in both trees.
50 | */
51 | public @Unsigned long getId() {
52 | return TSNode.id(self).address();
53 | }
54 |
55 | /** Get the numerical ID of the node's type. */
56 | public @Unsigned short getSymbol() {
57 | return ts_node_symbol(self);
58 | }
59 |
60 | /** Get the numerical ID of the node's type, as it appears in the grammar ignoring aliases. */
61 | public @Unsigned short getGrammarSymbol() {
62 | return ts_node_grammar_symbol(self);
63 | }
64 |
65 | /** Get the type of the node. */
66 | public String getType() {
67 | return ts_node_type(self).getString(0);
68 | }
69 |
70 | /** Get the type of the node, as it appears in the grammar ignoring aliases. */
71 | public String getGrammarType() {
72 | return ts_node_grammar_type(self).getString(0);
73 | }
74 |
75 | /**
76 | * Check if the node is named.
77 | *
78 | * Named nodes correspond to named rules in the grammar,
79 | * whereas anonymous nodes correspond to string literals.
80 | */
81 | public boolean isNamed() {
82 | return ts_node_is_named(self);
83 | }
84 |
85 | /**
86 | * Check if the node is extra.
87 | *
88 | *
Extra nodes represent things which are not required
89 | * by the grammar but can appear anywhere (e.g. whitespace).
90 | */
91 | public boolean isExtra() {
92 | return ts_node_is_extra(self);
93 | }
94 |
95 | /** Check if the node is an {@index ERROR} node. */
96 | public boolean isError() {
97 | return ts_node_is_error(self);
98 | }
99 |
100 | /**
101 | * Check if the node is {@index MISSING}.
102 | *
103 | *
MISSING nodes are inserted by the parser in order
104 | * to recover from certain kinds of syntax errors.
105 | */
106 | public boolean isMissing() {
107 | return ts_node_is_missing(self);
108 | }
109 |
110 | /** Check if the node has been edited. */
111 | public boolean hasChanges() {
112 | return ts_node_has_changes(self);
113 | }
114 |
115 | /**
116 | * Check if the node is an {@index ERROR},
117 | * or contains any {@index ERROR} nodes.
118 | */
119 | public boolean hasError() {
120 | return ts_node_has_error(self);
121 | }
122 |
123 | /** Get the parse state of this node. */
124 | public @Unsigned short getParseState() {
125 | return ts_node_parse_state(self);
126 | }
127 |
128 | /** Get the parse state after this node. */
129 | public @Unsigned short getNextParseState() {
130 | return ts_node_next_parse_state(self);
131 | }
132 |
133 | /** Get the start byte of the node. */
134 | public @Unsigned int getStartByte() {
135 | return ts_node_start_byte(self);
136 | }
137 |
138 | /** Get the end byte of the node. */
139 | public @Unsigned int getEndByte() {
140 | return ts_node_end_byte(self);
141 | }
142 |
143 | /** Get the range of the node. */
144 | public Range getRange() {
145 | return new Range(getStartPoint(), getEndPoint(), getStartByte(), getEndByte());
146 | }
147 |
148 | /** Get the start point of the node. */
149 | public Point getStartPoint() {
150 | return Point.from(ts_node_start_point(arena, self));
151 | }
152 |
153 | /** Get the end point of the node. */
154 | public Point getEndPoint() {
155 | return Point.from(ts_node_end_point(arena, self));
156 | }
157 |
158 | /** Get the number of this node's children. */
159 | public @Unsigned int getChildCount() {
160 | return ts_node_child_count(self);
161 | }
162 |
163 | /** Get the number of this node's named children. */
164 | public @Unsigned int getNamedChildCount() {
165 | return ts_node_named_child_count(self);
166 | }
167 |
168 | /** Get the number of this node's descendants, including the node itself. */
169 | public @Unsigned int getDescendantCount() {
170 | return ts_node_descendant_count(self);
171 | }
172 |
173 | /** The node's immediate parent, if any. */
174 | public Optional getParent() {
175 | return optional(ts_node_parent(arena, self));
176 | }
177 |
178 | /** The node's next sibling, if any. */
179 | public Optional getNextSibling() {
180 | return optional(ts_node_next_sibling(arena, self));
181 | }
182 |
183 | /** The node's previous sibling, if any. */
184 | public Optional getPrevSibling() {
185 | return optional(ts_node_prev_sibling(arena, self));
186 | }
187 |
188 | /** The node's next named sibling, if any. */
189 | public Optional getNextNamedSibling() {
190 | return optional(ts_node_next_named_sibling(arena, self));
191 | }
192 |
193 | /** The node's previous named sibling, if any. */
194 | public Optional getPrevNamedSibling() {
195 | return optional(ts_node_prev_named_sibling(arena, self));
196 | }
197 |
198 | /**
199 | * Get the node's child at the given index, if any.
200 | *
201 | * @apiNote This method is fairly fast, but its cost is technically
202 | * {@code log(i)}, so if you might be iterating over a long list of children,
203 | * you should use {@link #getChildren()} or {@link #walk()} instead.
204 | *
205 | * @throws IndexOutOfBoundsException If the index exceeds the
206 | * {@linkplain #getChildCount() child count}.
207 | */
208 | public Optional getChild(@Unsigned int index) throws IndexOutOfBoundsException {
209 | if (index >= getChildCount()) {
210 | throw new IndexOutOfBoundsException(
211 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index)));
212 | }
213 | return optional(ts_node_child(arena, self, index));
214 | }
215 |
216 | /**
217 | * Get the node's named child at the given index, if any.
218 | *
219 | * @apiNote This method is fairly fast, but its cost is technically
220 | * {@code log(i)}, so if you might be iterating over a long list of children,
221 | * you should use {@link #getNamedChildren()} or {@link #walk()} instead.
222 | *
223 | * @throws IndexOutOfBoundsException If the index exceeds the
224 | * {@linkplain #getNamedChildCount() child count}.
225 | */
226 | public Optional getNamedChild(@Unsigned int index) throws IndexOutOfBoundsException {
227 | if (index >= getNamedChildCount()) {
228 | throw new IndexOutOfBoundsException(
229 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index)));
230 | }
231 | return optional(ts_node_named_child(arena, self, index));
232 | }
233 |
234 | /**
235 | * Get the node's first child that contains or starts after the given byte offset.
236 | *
237 | * @since 0.25.0
238 | */
239 | public Optional getFirstChildForByte(@Unsigned int byte_offset) {
240 | return optional(ts_node_first_child_for_byte(arena, self, byte_offset));
241 | }
242 |
243 | /**
244 | * Get the node's first named child that contains or starts after the given byte offset.
245 | *
246 | * @since 0.25.0
247 | */
248 | public Optional getFirstNamedChildForByte(@Unsigned int byte_offset) {
249 | return optional(ts_node_first_named_child_for_byte(arena, self, byte_offset));
250 | }
251 |
252 | /**
253 | * Get the node's first child with the given field ID, if any.
254 | *
255 | * @see Language#getFieldIdForName
256 | */
257 | public Optional getChildByFieldId(@Unsigned short id) {
258 | return optional(ts_node_child_by_field_id(arena, self, id));
259 | }
260 |
261 | /** Get the node's first child with the given field name, if any. */
262 | public Optional getChildByFieldName(String name) {
263 | var segment = arena.allocateFrom(name);
264 | return optional(ts_node_child_by_field_name(arena, self, segment, name.length()));
265 | }
266 |
267 | /**
268 | * Get this node's children.
269 | *
270 | * @apiNote If you're walking the tree recursively, you may want to use {@link #walk()} instead.
271 | */
272 | public List getChildren() {
273 | if (this.children == null) {
274 | var length = getChildCount();
275 | if (length == 0) return Collections.emptyList();
276 | var children = new ArrayList(length);
277 | var cursor = ts_tree_cursor_new(arena, self);
278 | ts_tree_cursor_goto_first_child(cursor);
279 | for (int i = 0; i < length; ++i) {
280 | var node = ts_tree_cursor_current_node(arena, cursor);
281 | children.add(new Node(node, tree));
282 | ts_tree_cursor_goto_next_sibling(cursor);
283 | }
284 | ts_tree_cursor_delete(cursor);
285 | this.children = Collections.unmodifiableList(children);
286 | }
287 | return this.children;
288 | }
289 |
290 | /** Get this node's named children. */
291 | public List getNamedChildren() {
292 | return getChildren().stream().filter(Node::isNamed).toList();
293 | }
294 |
295 | /**
296 | * Get a list of the node's children with the given field ID.
297 | *
298 | * @see Language#getFieldIdForName
299 | */
300 | public List getChildrenByFieldId(@Unsigned short id) {
301 | if (id == 0) return Collections.emptyList();
302 | var length = getChildCount();
303 | var children = new ArrayList(length);
304 | var cursor = ts_tree_cursor_new(arena, self);
305 | var ok = ts_tree_cursor_goto_first_child(cursor);
306 | while (ok) {
307 | if (ts_tree_cursor_current_field_id(cursor) == id) {
308 | var node = ts_tree_cursor_current_node(arena, cursor);
309 | children.add(new Node(node, tree));
310 | }
311 | ok = ts_tree_cursor_goto_next_sibling(cursor);
312 | }
313 | ts_tree_cursor_delete(cursor);
314 | children.trimToSize();
315 | return children;
316 | }
317 |
318 | /** Get a list of the node's child with the given field name. */
319 | public List getChildrenByFieldName(String name) {
320 | return getChildrenByFieldId(tree.getLanguage().getFieldIdForName(name));
321 | }
322 |
323 | /**
324 | * Get the field name of this node’s child at the given index, if available.
325 | *
326 | * @throws IndexOutOfBoundsException If the index exceeds the
327 | * {@linkplain #getNamedChildCount() child count}.
328 | */
329 | public @Nullable String getFieldNameForChild(@Unsigned int index) throws IndexOutOfBoundsException {
330 | if (index >= getChildCount()) {
331 | throw new IndexOutOfBoundsException(
332 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index)));
333 | }
334 | var segment = ts_node_field_name_for_child(self, index);
335 | return segment.equals(MemorySegment.NULL) ? null : segment.getString(0);
336 | }
337 |
338 | /**
339 | * Get the field name of this node's named child at the given index, if available.
340 | *
341 | * @throws IndexOutOfBoundsException If the index exceeds the
342 | * {@linkplain #getNamedChildCount() child count}.
343 | * @since 0.24.0
344 | */
345 | public @Nullable String getFieldNameForNamedChild(@Unsigned int index) throws IndexOutOfBoundsException {
346 | if (index >= getChildCount()) {
347 | throw new IndexOutOfBoundsException(
348 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index)));
349 | }
350 | var segment = ts_node_field_name_for_named_child(self, index);
351 | return segment.equals(MemorySegment.NULL) ? null : segment.getString(0);
352 | }
353 |
354 | /**
355 | * Get the smallest node within this node that spans the given byte range, if any.
356 | *
357 | * @throws IllegalArgumentException If {@code start > end}.
358 | */
359 | public Optional getDescendant(@Unsigned int start, @Unsigned int end) throws IllegalArgumentException {
360 | if (Integer.compareUnsigned(start, end) > 0) {
361 | throw new IllegalArgumentException(String.format(
362 | "Start byte %s exceeds end byte %s",
363 | Integer.toUnsignedString(start), Integer.toUnsignedString(end)));
364 | }
365 | return optional(ts_node_descendant_for_byte_range(arena, self, start, end));
366 | }
367 |
368 | /**
369 | * Get the smallest node within this node that spans the given point range, if any.
370 | *
371 | * @throws IllegalArgumentException If {@code start > end}.
372 | */
373 | public Optional getDescendant(Point start, Point end) throws IllegalArgumentException {
374 | if (start.compareTo(end) > 0) {
375 | throw new IllegalArgumentException("Start point %s exceeds end point %s".formatted(start, end));
376 | }
377 | MemorySegment startPoint = start.into(arena), endPoint = end.into(arena);
378 | return optional(ts_node_descendant_for_point_range(arena, self, startPoint, endPoint));
379 | }
380 |
381 | /**
382 | * Get the smallest named node within this node that spans the given byte range, if any.
383 | *
384 | * @throws IllegalArgumentException If {@code start > end}.
385 | */
386 | public Optional getNamedDescendant(@Unsigned int start, @Unsigned int end) throws IllegalArgumentException {
387 | if (Integer.compareUnsigned(start, end) > 0) {
388 | throw new IllegalArgumentException(String.format(
389 | "Start byte %s exceeds end byte %s",
390 | Integer.toUnsignedString(start), Integer.toUnsignedString(end)));
391 | }
392 | return optional(ts_node_named_descendant_for_byte_range(arena, self, start, end));
393 | }
394 |
395 | /**
396 | * Get the smallest named node within this node that spans the given point range, if any.
397 | *
398 | * @throws IllegalArgumentException If {@code start > end}.
399 | */
400 | public Optional getNamedDescendant(Point start, Point end) {
401 | if (start.compareTo(end) > 0) {
402 | throw new IllegalArgumentException("Start point %s exceeds end point %s".formatted(start, end));
403 | }
404 | MemorySegment startPoint = start.into(arena), endPoint = end.into(arena);
405 | return optional(ts_node_named_descendant_for_point_range(arena, self, startPoint, endPoint));
406 | }
407 |
408 | /**
409 | * Get the node that contains the given descendant, if any.
410 | *
411 | * @since 0.24.0
412 | */
413 | public Optional getChildWithDescendant(Node descendant) {
414 | return optional(ts_node_child_with_descendant(arena, self, descendant.self));
415 | }
416 |
417 | /** Get the source code of the node, if available. */
418 | public @Nullable String getText() {
419 | return !wasEdited ? tree.getRegion(getStartByte(), getEndByte()) : null;
420 | }
421 |
422 | /**
423 | * Edit this node to keep it in-sync with source code that has been edited.
424 | *
425 | * @apiNote This method is only rarely needed. When you edit a syntax
426 | * tree via {@link Tree#edit}, all of the nodes that you retrieve from
427 | * the tree afterward will already reflect the edit. You only need
428 | * to use this when you have a specific {@linkplain Node} instance
429 | * that you want to keep and continue to use after an edit.
430 | */
431 | public void edit(InputEdit edit) {
432 | ts_node_edit(self, edit.into(arena));
433 | wasEdited = true;
434 | children = null;
435 | }
436 |
437 | /** Create a new {@linkplain TreeCursor tree cursor} starting from this node. */
438 | public TreeCursor walk() {
439 | return new TreeCursor(this, tree);
440 | }
441 |
442 | /** Get the S-expression representing the node. */
443 | public String toSexp() {
444 | var string = ts_node_string(self);
445 | var result = string.getString(0);
446 | free(string);
447 | return result;
448 | }
449 |
450 | /** Check if two nodes are identical. */
451 | @Override
452 | public boolean equals(Object o) {
453 | if (this == o) return true;
454 | if (!(o instanceof Node other)) return false;
455 | return ts_node_eq(self, other.self);
456 | }
457 |
458 | @Override
459 | public int hashCode() {
460 | return Long.hashCode(getId());
461 | }
462 |
463 | @Override
464 | public String toString() {
465 | return String.format(
466 | "Node{type=%s, startByte=%s, endByte=%s}",
467 | getType(), Integer.toUnsignedString(getStartByte()), Integer.toUnsignedString(getEndByte()));
468 | }
469 | }
470 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/ParseCallback.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import java.util.function.BiFunction;
4 | import org.jspecify.annotations.NonNull;
5 | import org.jspecify.annotations.Nullable;
6 |
7 | /** A function that retrieves a chunk of text at a given byte offset and point. */
8 | @FunctionalInterface
9 | public interface ParseCallback extends BiFunction {
10 | /**
11 | * {@inheritDoc}
12 | *
13 | * @param offset the current byte offset
14 | * @param point the current point
15 | * @return A chunk of text or {@code null} to indicate the end of the document.
16 | */
17 | @Override
18 | @Nullable
19 | String apply(@Unsigned Integer offset, @NonNull Point point);
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Parser.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import io.github.treesitter.jtreesitter.internal.*;
6 | import java.lang.foreign.Arena;
7 | import java.lang.foreign.MemoryLayout;
8 | import java.lang.foreign.MemorySegment;
9 | import java.util.Collections;
10 | import java.util.List;
11 | import java.util.Optional;
12 | import java.util.concurrent.atomic.AtomicLong;
13 | import java.util.function.Predicate;
14 | import org.jspecify.annotations.NullMarked;
15 | import org.jspecify.annotations.Nullable;
16 |
17 | /** A class that is used to produce a {@linkplain Tree syntax tree} from source code. */
18 | @NullMarked
19 | public final class Parser implements AutoCloseable {
20 | final MemorySegment self;
21 | private final Arena arena;
22 | private @Nullable Language language;
23 | private List includedRanges = Collections.singletonList(Range.DEFAULT);
24 |
25 | /**
26 | * Creates a new instance with a {@code null} language.
27 | *
28 | * @apiNote Parsing cannot be performed while the language is {@code null}.
29 | */
30 | public Parser() {
31 | arena = Arena.ofShared();
32 | self = ts_parser_new().reinterpret(arena, TreeSitter::ts_parser_delete);
33 | }
34 |
35 | /** Creates a new instance with the given language. */
36 | public Parser(Language language) {
37 | this();
38 | ts_parser_set_language(self, language.segment());
39 | this.language = language;
40 | }
41 |
42 | /** Get the language that the parser will use for parsing. */
43 | public @Nullable Language getLanguage() {
44 | return language;
45 | }
46 |
47 | /** Set the language that the parser will use for parsing. */
48 | public Parser setLanguage(Language language) {
49 | ts_parser_set_language(self, language.segment());
50 | this.language = language;
51 | return this;
52 | }
53 |
54 | /**
55 | * Get the maximum duration in microseconds that
56 | * parsing should be allowed to take before halting.
57 | *
58 | * @deprecated Use {@link Options} instead.
59 | */
60 | @Deprecated(since = "0.25.0")
61 | public @Unsigned long getTimeoutMicros() {
62 | return ts_parser_timeout_micros(self);
63 | }
64 |
65 | /**
66 | * Set the maximum duration in microseconds that
67 | * parsing should be allowed to take before halting.
68 | *
69 | * @deprecated Use {@link Options} instead.
70 | */
71 | @Deprecated(since = "0.25.0")
72 | @SuppressWarnings("DeprecatedIsStillUsed")
73 | public Parser setTimeoutMicros(@Unsigned long timeoutMicros) {
74 | ts_parser_set_timeout_micros(self, timeoutMicros);
75 | return this;
76 | }
77 |
78 | /**
79 | * Set the logger that the parser will use during parsing.
80 | *
81 | * Example
82 | *
83 | * {@snippet lang="java" :
84 | * import java.util.logging.Logger;
85 | *
86 | * Logger logger = Logger.getLogger("tree-sitter");
87 | * Parser parser = new Parser().setLogger(
88 | * (type, message) -> logger.info("%s - %s".formatted(type.name(), message)));
89 | * }
90 | */
91 | @SuppressWarnings("unused")
92 | public Parser setLogger(@Nullable Logger logger) {
93 | if (logger == null) {
94 | ts_parser_set_logger(self, TSLogger.allocate(arena));
95 | } else {
96 | var segment = TSLogger.allocate(arena);
97 | TSLogger.payload(segment, MemorySegment.NULL);
98 | // NOTE: can't use _ because of palantir/palantir-java-format#934
99 | var log = TSLogger.log.allocate(
100 | (p, type, message) -> {
101 | var logType = Logger.Type.values()[type];
102 | logger.accept(logType, message.getString(0));
103 | },
104 | arena);
105 | TSLogger.log(segment, log);
106 | ts_parser_set_logger(self, segment);
107 | }
108 | return this;
109 | }
110 |
111 | /**
112 | * Set the parser's current cancellation flag.
113 | *
114 | *
The parser will periodically read from this flag during parsing.
115 | * If it reads a non-zero value, it will halt early.
116 | *
117 | * @deprecated Use {@link Options} instead.
118 | */
119 | @Deprecated(since = "0.25.0")
120 | @SuppressWarnings("DeprecatedIsStillUsed")
121 | public synchronized Parser setCancellationFlag(CancellationFlag cancellationFlag) {
122 | ts_parser_set_cancellation_flag(self, cancellationFlag.segment);
123 | return this;
124 | }
125 |
126 | /**
127 | * Get the ranges of text that the parser should include when parsing.
128 | *
129 | * @apiNote By default, the parser will always include entire documents.
130 | */
131 | public List getIncludedRanges() {
132 | return includedRanges;
133 | }
134 |
135 | /**
136 | * Set the ranges of text that the parser should include when parsing.
137 | *
138 | * This allows you to parse only a portion of a document
139 | * but still return a syntax tree whose ranges match up with the
140 | * document as a whole. You can also pass multiple disjoint ranges.
141 | *
142 | * @throws IllegalArgumentException If the ranges overlap or are not in ascending order.
143 | */
144 | public Parser setIncludedRanges(List includedRanges) {
145 | var size = includedRanges.size();
146 | if (size > 0) {
147 | try (var arena = Arena.ofConfined()) {
148 | var layout = MemoryLayout.sequenceLayout(size, TSRange.layout());
149 | var ranges = arena.allocate(layout);
150 |
151 | var startRow = layout.varHandle(
152 | MemoryLayout.PathElement.sequenceElement(),
153 | MemoryLayout.PathElement.groupElement("start_point"),
154 | MemoryLayout.PathElement.groupElement("row"));
155 | var startColumn = layout.varHandle(
156 | MemoryLayout.PathElement.sequenceElement(),
157 | MemoryLayout.PathElement.groupElement("start_point"),
158 | MemoryLayout.PathElement.groupElement("column"));
159 | var endRow = layout.varHandle(
160 | MemoryLayout.PathElement.sequenceElement(),
161 | MemoryLayout.PathElement.groupElement("end_point"),
162 | MemoryLayout.PathElement.groupElement("row"));
163 | var endColumn = layout.varHandle(
164 | MemoryLayout.PathElement.sequenceElement(),
165 | MemoryLayout.PathElement.groupElement("end_point"),
166 | MemoryLayout.PathElement.groupElement("column"));
167 | var startByte = layout.varHandle(
168 | MemoryLayout.PathElement.sequenceElement(),
169 | MemoryLayout.PathElement.groupElement("start_byte"));
170 | var endByte = layout.varHandle(
171 | MemoryLayout.PathElement.sequenceElement(), /**/
172 | MemoryLayout.PathElement.groupElement("end_byte"));
173 |
174 | for (int i = 0; i < size; ++i) {
175 | var range = includedRanges.get(i).into(arena);
176 | var startPoint = TSRange.start_point(range);
177 | var endPoint = TSRange.end_point(range);
178 | startByte.set(ranges, 0L, (long) i, TSRange.start_byte(range));
179 | endByte.set(ranges, 0L, (long) i, TSRange.end_byte(range));
180 | startRow.set(ranges, 0L, (long) i, TSPoint.row(startPoint));
181 | startColumn.set(ranges, 0L, (long) i, TSPoint.column(startPoint));
182 | endRow.set(ranges, 0L, (long) i, TSPoint.row(endPoint));
183 | endColumn.set(ranges, 0L, (long) i, TSPoint.column(endPoint));
184 | }
185 |
186 | if (!ts_parser_set_included_ranges(self, ranges, size)) {
187 | throw new IllegalArgumentException(
188 | "Included ranges must be in ascending order and must not overlap");
189 | }
190 | }
191 | this.includedRanges = List.copyOf(includedRanges);
192 | } else {
193 | ts_parser_set_included_ranges(self, MemorySegment.NULL, 0);
194 | this.includedRanges = Collections.singletonList(Range.DEFAULT);
195 | }
196 | return this;
197 | }
198 |
199 | /**
200 | * Parse source code from a string and create a syntax tree.
201 | *
202 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
203 | * @throws IllegalStateException If the parser does not have a language assigned.
204 | */
205 | public Optional parse(String source) throws IllegalStateException {
206 | return parse(source, InputEncoding.UTF_8);
207 | }
208 |
209 | /**
210 | * Parse source code from a string and create a syntax tree.
211 | *
212 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
213 | * @throws IllegalStateException If the parser does not have a language assigned.
214 | */
215 | public Optional parse(String source, InputEncoding encoding) throws IllegalStateException {
216 | return parse(source, encoding, null);
217 | }
218 |
219 | /**
220 | * Parse source code from a string and create a syntax tree.
221 | *
222 | * If you have already parsed an earlier version of this document and the
223 | * document has since been edited, pass the previous syntax tree to {@code oldTree}
224 | * so that the unchanged parts of it can be reused. This will save time and memory.
225 | *
For this to work correctly, you must have already edited the old syntax tree using
226 | * the {@link Tree#edit} method in a way that exactly matches the source code changes.
227 | *
228 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
229 | * @throws IllegalStateException If the parser does not have a language assigned.
230 | */
231 | public Optional parse(String source, Tree oldTree) throws IllegalStateException {
232 | return parse(source, InputEncoding.UTF_8, oldTree);
233 | }
234 |
235 | /**
236 | * Parse source code from a string and create a syntax tree.
237 | *
238 | * If you have already parsed an earlier version of this document and the
239 | * document has since been edited, pass the previous syntax tree to {@code oldTree}
240 | * so that the unchanged parts of it can be reused. This will save time and memory.
241 | *
For this to work correctly, you must have already edited the old syntax tree using
242 | * the {@link Tree#edit} method in a way that exactly matches the source code changes.
243 | *
244 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
245 | * @throws IllegalStateException If the parser does not have a language assigned.
246 | */
247 | public Optional parse(String source, InputEncoding encoding, @Nullable Tree oldTree)
248 | throws IllegalStateException {
249 | if (language == null) {
250 | throw new IllegalStateException("The parser has no language assigned");
251 | }
252 |
253 | try (var alloc = Arena.ofShared()) {
254 | var bytes = source.getBytes(encoding.charset());
255 | var string = alloc.allocateFrom(C_CHAR, bytes);
256 | var old = oldTree == null ? MemorySegment.NULL : oldTree.segment();
257 | var tree = ts_parser_parse_string_encoding(self, old, string, bytes.length, encoding.ordinal());
258 | if (tree.equals(MemorySegment.NULL)) return Optional.empty();
259 | return Optional.of(new Tree(tree, language, source, encoding.charset()));
260 | }
261 | }
262 |
263 | /**
264 | * Parse source code from a callback and create a syntax tree.
265 | *
266 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
267 | * @throws IllegalStateException If the parser does not have a language assigned.
268 | */
269 | public Optional parse(ParseCallback parseCallback, InputEncoding encoding) throws IllegalStateException {
270 | return parse(parseCallback, encoding, null, null);
271 | }
272 |
273 | /**
274 | * Parse source code from a callback and create a syntax tree.
275 | *
276 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
277 | * @throws IllegalStateException If the parser does not have a language assigned.
278 | */
279 | public Optional parse(ParseCallback parseCallback, InputEncoding encoding, Options options)
280 | throws IllegalStateException {
281 | return parse(parseCallback, encoding, null, options);
282 | }
283 |
284 | /**
285 | * Parse source code from a callback and create a syntax tree.
286 | *
287 | * If you have already parsed an earlier version of this document and the
288 | * document has since been edited, pass the previous syntax tree to {@code oldTree}
289 | * so that the unchanged parts of it can be reused. This will save time and memory.
290 | *
For this to work correctly, you must have already edited the old syntax tree using
291 | * the {@link Tree#edit} method in a way that exactly matches the source code changes.
292 | *
293 | * @return An optional {@linkplain Tree} which is empty if parsing was halted.
294 | * @throws IllegalStateException If the parser does not have a language assigned.
295 | */
296 | @SuppressWarnings("unused")
297 | public Optional parse(
298 | ParseCallback parseCallback, InputEncoding encoding, @Nullable Tree oldTree, @Nullable Options options)
299 | throws IllegalStateException {
300 | if (language == null) {
301 | throw new IllegalStateException("The parser has no language assigned");
302 | }
303 |
304 | var input = TSInput.allocate(arena);
305 | TSInput.payload(input, MemorySegment.NULL);
306 | TSInput.encoding(input, encoding.ordinal());
307 | // NOTE: can't use _ because of palantir/palantir-java-format#934
308 | var read = TSInput.read.allocate(
309 | (payload, index, point, bytes) -> {
310 | var result = parseCallback.apply(index, Point.from(point));
311 | if (result == null) {
312 | bytes.set(C_INT, 0, 0);
313 | return MemorySegment.NULL;
314 | }
315 | var buffer = result.getBytes(encoding.charset());
316 | bytes.set(C_INT, 0, buffer.length);
317 | return arena.allocateFrom(C_CHAR, buffer);
318 | },
319 | arena);
320 | TSInput.read(input, read);
321 |
322 | MemorySegment tree, old = oldTree == null ? MemorySegment.NULL : oldTree.segment();
323 | if (options == null) {
324 | tree = ts_parser_parse(self, old, input);
325 | } else {
326 | var parseOptions = TSParseOptions.allocate(arena);
327 | TSParseOptions.payload(parseOptions, MemorySegment.NULL);
328 | var progress = TSParseOptions.progress_callback.allocate(
329 | (payload) -> {
330 | var offset = TSParseState.current_byte_offset(payload);
331 | var hasError = TSParseState.has_error(payload);
332 | return options.progressCallback(new State(offset, hasError));
333 | },
334 | arena);
335 | TSParseOptions.progress_callback(parseOptions, progress);
336 | tree = ts_parser_parse_with_options(self, old, input, parseOptions);
337 | }
338 | if (tree.equals(MemorySegment.NULL)) return Optional.empty();
339 | return Optional.of(new Tree(tree, language, null, null));
340 | }
341 |
342 | /**
343 | * Instruct the parser to start the next {@linkplain #parse parse} from the beginning.
344 | *
345 | * @apiNote If parsing was previously halted, the parser will resume where it left off.
346 | * If you intend to parse another document instead, you must call this method first.
347 | */
348 | public void reset() {
349 | ts_parser_reset(self);
350 | }
351 |
352 | @Override
353 | public void close() throws RuntimeException {
354 | arena.close();
355 | }
356 |
357 | @Override
358 | public String toString() {
359 | return "Parser{language=%s}".formatted(language);
360 | }
361 |
362 | /**
363 | * A class representing the current state of the parser.
364 | *
365 | * @since 0.25.0
366 | */
367 | public static final class State {
368 | private final @Unsigned int currentByteOffset;
369 | private final boolean hasError;
370 |
371 | private State(@Unsigned int currentByteOffset, boolean hasError) {
372 | this.currentByteOffset = currentByteOffset;
373 | this.hasError = hasError;
374 | }
375 |
376 | /** Get the current byte offset of the parser. */
377 | public @Unsigned int getCurrentByteOffset() {
378 | return currentByteOffset;
379 | }
380 |
381 | /** Check if the parser has encountered an error. */
382 | public boolean hasError() {
383 | return hasError;
384 | }
385 |
386 | @Override
387 | public String toString() {
388 | return String.format(
389 | "Parser.State{currentByteOffset=%s, hasError=%s}",
390 | Integer.toUnsignedString(currentByteOffset), hasError);
391 | }
392 | }
393 |
394 | /**
395 | * A class representing the parser options.
396 | *
397 | * @since 0.25.0
398 | */
399 | @NullMarked
400 | public static final class Options {
401 | private final Predicate progressCallback;
402 |
403 | public Options(Predicate progressCallback) {
404 | this.progressCallback = progressCallback;
405 | }
406 |
407 | private boolean progressCallback(State state) {
408 | return progressCallback.test(state);
409 | }
410 | }
411 |
412 | /**
413 | * A class representing a cancellation flag.
414 | *
415 | * @deprecated Use {@link Options} instead.
416 | */
417 | @Deprecated(since = "0.25.0")
418 | @SuppressWarnings("DeprecatedIsStillUsed")
419 | public static class CancellationFlag {
420 | private final Arena arena = Arena.ofAuto();
421 | private final MemorySegment segment = arena.allocate(C_LONG_LONG);
422 | private final AtomicLong value = new AtomicLong();
423 |
424 | /** Creates an uninitialized cancellation flag. */
425 | public CancellationFlag() {}
426 |
427 | /** Get the value of the flag. */
428 | public long get() {
429 | return value.get();
430 | }
431 |
432 | /** Set the value of the flag. */
433 | @SuppressWarnings("unused")
434 | public void set(long value) {
435 | // NOTE: can't use _ because of palantir/palantir-java-format#934
436 | segment.set(C_LONG_LONG, 0L, this.value.updateAndGet(o -> value));
437 | }
438 | }
439 | }
440 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Point.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import io.github.treesitter.jtreesitter.internal.TSPoint;
4 | import java.lang.foreign.MemorySegment;
5 | import java.lang.foreign.SegmentAllocator;
6 |
7 | /**
8 | * A position in a text document in terms of rows and columns.
9 | *
10 | * @param row The zero-based row of the document.
11 | * @param column The zero-based column of the document.
12 | */
13 | public record Point(@Unsigned int row, @Unsigned int column) implements Comparable {
14 | /** The minimum value a {@linkplain Point} can have. */
15 | public static final Point MIN = new Point(0, 0);
16 |
17 | /** The maximum value a {@linkplain Point} can have. */
18 | public static final Point MAX = new Point(-1, -1);
19 |
20 | static Point from(MemorySegment point) {
21 | return new Point(TSPoint.row(point), TSPoint.column(point));
22 | }
23 |
24 | MemorySegment into(SegmentAllocator allocator) {
25 | var point = TSPoint.allocate(allocator);
26 | TSPoint.row(point, row);
27 | TSPoint.column(point, column);
28 | return point;
29 | }
30 |
31 | @Override
32 | public int compareTo(Point other) {
33 | var rowDiff = Integer.compareUnsigned(row, other.row);
34 | if (rowDiff != 0) return rowDiff;
35 | return Integer.compareUnsigned(column, other.column);
36 | }
37 |
38 | @Override
39 | public String toString() {
40 | return "Point[row=%s, column=%s]".formatted(Integer.toUnsignedString(row), Integer.toUnsignedString(column));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/QueryCapture.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import org.jspecify.annotations.NullMarked;
4 |
5 | /**
6 | * A {@link Node} that was captured with a certain capture name.
7 | *
8 | * @param name The name of the capture.
9 | * @param node The captured node.
10 | */
11 | @NullMarked
12 | public record QueryCapture(String name, Node node) {}
13 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import io.github.treesitter.jtreesitter.internal.*;
6 | import java.lang.foreign.Arena;
7 | import java.lang.foreign.MemorySegment;
8 | import java.lang.foreign.SegmentAllocator;
9 | import java.util.AbstractMap.SimpleImmutableEntry;
10 | import java.util.function.BiPredicate;
11 | import java.util.function.Predicate;
12 | import java.util.stream.Stream;
13 | import java.util.stream.StreamSupport;
14 | import org.jspecify.annotations.NullMarked;
15 | import org.jspecify.annotations.Nullable;
16 |
17 | /**
18 | * A class that can be used to execute a {@linkplain Query query}
19 | * on a {@linkplain Tree syntax tree}.
20 | *
21 | * @since 0.25.0
22 | */
23 | @NullMarked
24 | public class QueryCursor implements AutoCloseable {
25 | private final MemorySegment self;
26 | private final Arena arena;
27 | private final Query query;
28 |
29 | /** Create a new cursor for the given query. */
30 | public QueryCursor(Query query) {
31 | this.query = query;
32 | arena = Arena.ofShared();
33 | self = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete);
34 | }
35 |
36 | /**
37 | * Get the maximum number of in-progress matches.
38 | *
39 | * @apiNote Defaults to {@code -1} (unlimited).
40 | */
41 | public @Unsigned int getMatchLimit() {
42 | return ts_query_cursor_match_limit(self);
43 | }
44 |
45 | /**
46 | * Get the maximum number of in-progress matches.
47 | *
48 | * @throws IllegalArgumentException If {@code matchLimit == 0}.
49 | */
50 | public QueryCursor setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentException {
51 | if (matchLimit == 0) {
52 | throw new IllegalArgumentException("The match limit cannot equal 0");
53 | }
54 | ts_query_cursor_set_match_limit(self, matchLimit);
55 | return this;
56 | }
57 |
58 | /**
59 | * Get the maximum duration in microseconds that query
60 | * execution should be allowed to take before halting.
61 | *
62 | * @apiNote Defaults to {@code 0} (unlimited).
63 | *
64 | * @deprecated Use {@link Options} instead.
65 | */
66 | @Deprecated(since = "0.25.0")
67 | public @Unsigned long getTimeoutMicros() {
68 | return ts_query_cursor_timeout_micros(self);
69 | }
70 |
71 | /**
72 | * Set the maximum duration in microseconds that query
73 | * execution should be allowed to take before halting.
74 | *
75 | * @deprecated Use {@link Options} instead.
76 | */
77 | @Deprecated(since = "0.25.0")
78 | public QueryCursor setTimeoutMicros(@Unsigned long timeoutMicros) {
79 | ts_query_cursor_set_timeout_micros(self, timeoutMicros);
80 | return this;
81 | }
82 |
83 | /**
84 | * Set the maximum start depth for the query.
85 | *
86 | * This prevents cursors from exploring children nodes at a certain depth.
87 | *
Note that if a pattern includes many children, then they will still be checked.
88 | */
89 | public QueryCursor setMaxStartDepth(@Unsigned int maxStartDepth) {
90 | ts_query_cursor_set_max_start_depth(self, maxStartDepth);
91 | return this;
92 | }
93 |
94 | /**
95 | * Set the range of bytes in which the query will be executed.
96 | *
The query cursor will return matches that intersect with the given range.
97 | * This means that a match may be returned even if some of its captures fall
98 | * outside the specified range, as long as at least part of the match
99 | * overlaps with the range.
100 | *
101 | *
For example, if a query pattern matches a node that spans a larger area
102 | * than the specified range, but part of that node intersects with the range,
103 | * the entire match will be returned.
104 | *
105 | * @throws IllegalArgumentException If `endByte > startByte`.
106 | */
107 | public QueryCursor setByteRange(@Unsigned int startByte, @Unsigned int endByte) throws IllegalArgumentException {
108 | if (!ts_query_cursor_set_byte_range(self, startByte, endByte)) {
109 | throw new IllegalArgumentException("Invalid byte range");
110 | }
111 | return this;
112 | }
113 |
114 | /**
115 | * Set the range of points in which the query will be executed.
116 | *
117 | *
The query cursor will return matches that intersect with the given range.
118 | * This means that a match may be returned even if some of its captures fall
119 | * outside the specified range, as long as at least part of the match
120 | * overlaps with the range.
121 | *
122 | *
For example, if a query pattern matches a node that spans a larger area
123 | * than the specified range, but part of that node intersects with the range,
124 | * the entire match will be returned.
125 | *
126 | * @throws IllegalArgumentException If `endPoint > startPoint`.
127 | */
128 | public QueryCursor setPointRange(Point startPoint, Point endPoint) throws IllegalArgumentException {
129 | try (var alloc = Arena.ofConfined()) {
130 | MemorySegment start = startPoint.into(alloc), end = endPoint.into(alloc);
131 | if (!ts_query_cursor_set_point_range(self, start, end)) {
132 | throw new IllegalArgumentException("Invalid point range");
133 | }
134 | }
135 | return this;
136 | }
137 |
138 | /**
139 | * Check if the query exceeded its maximum number of
140 | * in-progress matches during its last execution.
141 | */
142 | public boolean didExceedMatchLimit() {
143 | return ts_query_cursor_did_exceed_match_limit(self);
144 | }
145 |
146 | private void exec(Node node, @Nullable Options options) {
147 | try (var alloc = Arena.ofConfined()) {
148 | if (options == null || options.progressCallback == null) {
149 | ts_query_cursor_exec(self, query.segment(), node.copy(alloc));
150 | } else {
151 | var cursorOptions = TSQueryCursorOptions.allocate(alloc);
152 | TSQueryCursorOptions.payload(cursorOptions, MemorySegment.NULL);
153 | var progress = TSQueryCursorOptions.progress_callback.allocate(
154 | (payload) -> {
155 | var offset = TSQueryCursorState.current_byte_offset(payload);
156 | return options.progressCallback.test(new State(offset));
157 | },
158 | alloc);
159 | TSQueryCursorOptions.progress_callback(cursorOptions, progress);
160 | ts_query_cursor_exec_with_options(self, query.segment(), node.copy(alloc), cursorOptions);
161 | }
162 | }
163 | }
164 |
165 | /**
166 | * Iterate over all the captures in the order that they were found.
167 | *
168 | *
This is useful if you don't care about which pattern matched,
169 | * and just want a single, ordered sequence of captures.
170 | *
171 | * @param node The node that the query will run on.
172 | *
173 | * @implNote The lifetime of the matches is bound to that of the cursor.
174 | */
175 | public Stream> findCaptures(Node node) {
176 | return findCaptures(node, arena, new Options(null, null));
177 | }
178 |
179 | /**
180 | * Iterate over all the captures in the order that they were found.
181 | *
182 | * This is useful if you don't care about which pattern matched,
183 | * and just want a single, ordered sequence of captures.
184 | *
185 | * @param node The node that the query will run on.
186 | *
187 | * @implNote The lifetime of the matches is bound to that of the cursor.
188 | */
189 | public Stream> findCaptures(Node node, Options options) {
190 | return findCaptures(node, arena, options);
191 | }
192 |
193 | /**
194 | * Iterate over all the captures in the order that they were found.
195 | *
196 | * This is useful if you don't care about which pattern matched,
197 | * and just want a single, ordered sequence of captures.
198 | *
199 | * @param node The node that the query will run on.
200 | */
201 | public Stream> findCaptures(
202 | Node node, SegmentAllocator allocator, Options options) {
203 | exec(node, options);
204 | var iterator = new CapturesIterator(query, self, node.getTree(), allocator, options.predicateCallback);
205 | return StreamSupport.stream(iterator, false);
206 | }
207 |
208 | /**
209 | * Iterate over all the matches in the order that they were found.
210 | *
211 | * Because multiple patterns can match the same set of nodes, one match may contain
212 | * captures that appear before some of the captures from a previous match.
213 | *
214 | * @param node The node that the query will run on.
215 | *
216 | * @implNote The lifetime of the matches is bound to that of the cursor.
217 | */
218 | public Stream findMatches(Node node) {
219 | return findMatches(node, arena, new Options(null, null));
220 | }
221 |
222 | /**
223 | * Iterate over all the matches in the order that they were found.
224 | *
225 | * Because multiple patterns can match the same set of nodes, one match may contain
226 | * captures that appear before some of the captures from a previous match.
227 | *
228 | *
Predicate Example
229 | *
230 | * {@snippet lang = "java":
231 | * QueryCursor.Options options = new QueryCursor.Options((predicate, match) -> {
232 | * if (!predicate.getName().equals("ieq?")) return true;
233 | * List args = predicate.getArgs();
234 | * Node node = match.findNodes(args.getFirst().value()).getFirst();
235 | * return args.getLast().value().equalsIgnoreCase(node.getText());
236 | * });
237 | * Stream matches = self.findMatches(tree.getRootNode(), options);
238 | *}
239 | *
240 | * @param node The node that the query will run on.
241 | *
242 | * @implNote The lifetime of the matches is bound to that of the cursor.
243 | */
244 | public Stream findMatches(Node node, Options options) {
245 | return findMatches(node, arena, options);
246 | }
247 |
248 | /**
249 | * Iterate over all the matches in the order that they were found, using the given allocator.
250 | *
251 | * Because multiple patterns can match the same set of nodes, one match may contain
252 | * captures that appear before some of the captures from a previous match.
253 | *
254 | * @param node The node that the query will run on.
255 | *
256 | * @see #findMatches(Node, Options)
257 | */
258 | public Stream findMatches(Node node, SegmentAllocator allocator, Options options) {
259 | exec(node, options);
260 | var iterator = new MatchesIterator(query, self, node.getTree(), allocator, options.predicateCallback);
261 | return StreamSupport.stream(iterator, false);
262 | }
263 |
264 | @Override
265 | public void close() throws RuntimeException {
266 | arena.close();
267 | }
268 |
269 | /** A class representing the current state of the query cursor. */
270 | public static final class State {
271 | private final @Unsigned int currentByteOffset;
272 |
273 | private State(@Unsigned int currentByteOffset) {
274 | this.currentByteOffset = currentByteOffset;
275 | }
276 |
277 | /** Get the current byte offset of the cursor. */
278 | public @Unsigned int getCurrentByteOffset() {
279 | return currentByteOffset;
280 | }
281 |
282 | @Override
283 | public String toString() {
284 | return String.format(
285 | "QueryCursor.State{currentByteOffset=%s}", Integer.toUnsignedString(currentByteOffset));
286 | }
287 | }
288 |
289 | /** A class representing the query cursor options. */
290 | @NullMarked
291 | public static class Options {
292 | private final @Nullable Predicate progressCallback;
293 | private final @Nullable BiPredicate predicateCallback;
294 |
295 | /**
296 | * @param progressCallback Progress handler.
297 | * @param predicateCallback Custom predicate handler.
298 | */
299 | private Options(
300 | @Nullable Predicate progressCallback,
301 | @Nullable BiPredicate predicateCallback) {
302 | this.progressCallback = progressCallback;
303 | this.predicateCallback = predicateCallback;
304 | }
305 |
306 | /**
307 | * @param progressCallback Progress handler.
308 | */
309 | public Options(Predicate progressCallback) {
310 | this.progressCallback = progressCallback;
311 | this.predicateCallback = null;
312 | }
313 |
314 | /**
315 | * @param predicateCallback Custom predicate handler.
316 | */
317 | public Options(BiPredicate predicateCallback) {
318 | this.progressCallback = null;
319 | this.predicateCallback = predicateCallback;
320 | }
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/QueryError.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import org.jspecify.annotations.NonNull;
4 |
5 | /** Any error that occurred while instantiating a {@link Query}. */
6 | public abstract sealed class QueryError extends IllegalArgumentException
7 | permits QueryError.Capture,
8 | QueryError.Field,
9 | QueryError.NodeType,
10 | QueryError.Structure,
11 | QueryError.Syntax,
12 | QueryError.Predicate {
13 |
14 | protected QueryError(@NonNull String message, Throwable cause) {
15 | super(message, cause);
16 | }
17 |
18 | protected QueryError(@NonNull String message) {
19 | super(message, null);
20 | }
21 |
22 | /** A query syntax error. */
23 | public static final class Syntax extends QueryError {
24 | Syntax() {
25 | super("Unexpected EOF");
26 | }
27 |
28 | Syntax(long row, long column) {
29 | super("Invalid syntax at row %d, column %d".formatted(row, column));
30 | }
31 | }
32 |
33 | /** A capture name error. */
34 | public static final class Capture extends QueryError {
35 | Capture(long row, long column, @NonNull CharSequence capture) {
36 | super("Invalid capture name at row %d, column %d: %s".formatted(row, column, capture));
37 | }
38 | }
39 |
40 | /** A field name error. */
41 | public static final class Field extends QueryError {
42 | Field(long row, long column, @NonNull CharSequence field) {
43 | super("Invalid field name at row %d, column %d: %s".formatted(row, column, field));
44 | }
45 | }
46 |
47 | /** A node type error. */
48 | public static final class NodeType extends QueryError {
49 | NodeType(long row, long column, @NonNull CharSequence type) {
50 | super("Invalid node type at row %d, column %d: %s".formatted(row, column, type));
51 | }
52 | }
53 |
54 | /** A pattern structure error. */
55 | public static final class Structure extends QueryError {
56 | Structure(long row, long column) {
57 | super("Impossible pattern at row %d, column %d".formatted(row, column));
58 | }
59 | }
60 |
61 | /** A query predicate error. */
62 | public static final class Predicate extends QueryError {
63 | Predicate(long row, @NonNull String details, Throwable cause) {
64 | super("Invalid predicate in pattern at row %d: %s".formatted(row, details), cause);
65 | }
66 |
67 | Predicate(long row, @NonNull String format, Object... args) {
68 | this(row, String.format(format, args), (Throwable) null);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import io.github.treesitter.jtreesitter.internal.*;
4 | import java.lang.foreign.MemorySegment;
5 | import java.lang.foreign.SegmentAllocator;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import org.jspecify.annotations.NullMarked;
9 |
10 | /** A match that corresponds to a certain pattern in the query. */
11 | @NullMarked
12 | public record QueryMatch(@Unsigned int patternIndex, List captures) {
13 | /** Creates an instance of a QueryMatch record class. */
14 | public QueryMatch(@Unsigned int patternIndex, List captures) {
15 | this.patternIndex = patternIndex;
16 | this.captures = List.copyOf(captures);
17 | }
18 |
19 | static QueryMatch from(MemorySegment match, List captureNames, Tree tree, SegmentAllocator allocator) {
20 | var count = Short.toUnsignedInt(TSQueryMatch.capture_count(match));
21 | var matchCaptures = TSQueryMatch.captures(match);
22 | var captureList = new ArrayList(count);
23 | for (int i = 0; i < count; ++i) {
24 | var capture = TSQueryCapture.asSlice(matchCaptures, i);
25 | var name = captureNames.get(TSQueryCapture.index(capture));
26 | var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture));
27 | captureList.add(new QueryCapture(name, new Node(node, tree)));
28 | }
29 | var patternIndex = TSQueryMatch.pattern_index(match);
30 | return new QueryMatch(patternIndex, captureList);
31 | }
32 |
33 | /** Find the nodes that are captured by the given capture name. */
34 | public List findNodes(String capture) {
35 | return captures.stream()
36 | .filter(c -> c.name().equals(capture))
37 | .map(QueryCapture::node)
38 | .toList();
39 | }
40 |
41 | @Override
42 | public String toString() {
43 | return String.format(
44 | "QueryMatch[patternIndex=%s, captures=%s]", Integer.toUnsignedString(patternIndex), captures);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/QueryPredicate.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import java.util.*;
4 | import java.util.function.Predicate;
5 | import java.util.regex.Pattern;
6 | import org.jspecify.annotations.NullMarked;
7 |
8 | /**
9 | * A query predicate that associates conditions (or arbitrary metadata) with a pattern.
10 | *
11 | * @see Predicates
12 | */
13 | @NullMarked
14 | public sealed class QueryPredicate permits QueryPredicate.AnyOf, QueryPredicate.Eq, QueryPredicate.Match {
15 | private final String name;
16 | protected final List args;
17 |
18 | protected QueryPredicate(String name, int argc) {
19 | this(name, new ArrayList<>(argc));
20 | }
21 |
22 | QueryPredicate(String name, List args) {
23 | this.name = name;
24 | this.args = args;
25 | }
26 |
27 | /** Get the name of the predicate. */
28 | public String getName() {
29 | return name;
30 | }
31 |
32 | /** Get the arguments given to the predicate. */
33 | public List getArgs() {
34 | return Collections.unmodifiableList(args);
35 | }
36 |
37 | boolean test(QueryMatch queryMatch) {
38 | return true;
39 | }
40 |
41 | @Override
42 | public String toString() {
43 | return "QueryPredicate{name=%s, args=%s}".formatted(name, args);
44 | }
45 |
46 | /**
47 | * Handles the following predicates:
48 | * {@code #eq?}, {@code #not-eq?}, {@code #any-eq?}, {@code #any-not-eq?}
49 | */
50 | @NullMarked
51 | public static final class Eq extends QueryPredicate {
52 | private final String capture;
53 | private final String value;
54 | private final boolean isPositive;
55 | private final boolean isAny;
56 | private final boolean isCapture;
57 |
58 | static final Set NAMES = Set.of("eq?", "not-eq?", "any-eq?", "any-not-eq?");
59 |
60 | Eq(String name, String capture, String value, boolean isCapture) {
61 | super(name, 2);
62 | this.capture = capture;
63 | this.value = value;
64 | this.isPositive = !name.contains("not-");
65 | this.isAny = name.startsWith("any-");
66 | this.isCapture = isCapture;
67 |
68 | args.add(new QueryPredicateArg.Capture(capture));
69 | if (isCapture) args.add(new QueryPredicateArg.Capture(value));
70 | else args.add(new QueryPredicateArg.Literal(value));
71 | }
72 |
73 | @Override
74 | boolean test(QueryMatch match) {
75 | return isCapture ? testCapture(match) : testLiteral(match);
76 | }
77 |
78 | private boolean testCapture(QueryMatch match) {
79 | var findNodes1 = match.findNodes(capture).stream();
80 | var findNodes2 = match.findNodes(value).stream();
81 | Predicate predicate =
82 | n1 -> findNodes2.anyMatch(n2 -> Objects.equals(n1.getText(), n2.getText()) == isPositive);
83 | return isAny ? findNodes1.anyMatch(predicate) : findNodes1.allMatch(predicate);
84 | }
85 |
86 | private boolean testLiteral(QueryMatch match) {
87 | var findNodes1 = match.findNodes(capture);
88 | if (findNodes1.isEmpty()) return !isPositive;
89 | Predicate predicate = node -> {
90 | var text = Objects.requireNonNull(node.getText());
91 | return value.equals(text) == isPositive;
92 | };
93 | if (!isAny) return findNodes1.stream().allMatch(predicate);
94 | return findNodes1.stream().anyMatch(predicate);
95 | }
96 | }
97 |
98 | /**
99 | * Handles the following predicates:
100 | * {@code #match?}, {@code #not-match?}, {@code #any-match?}, {@code #any-not-match?}
101 | */
102 | @NullMarked
103 | public static final class Match extends QueryPredicate {
104 | private final String capture;
105 | private final Pattern pattern;
106 | private final boolean isPositive;
107 | private final boolean isAny;
108 |
109 | static final Set NAMES = Set.of("match?", "not-match?", "any-match?", "any-not-match?");
110 |
111 | Match(String name, String capture, Pattern pattern) {
112 | super(name, 2);
113 | this.capture = capture;
114 | this.pattern = pattern;
115 | this.isPositive = !name.contains("not-");
116 | this.isAny = name.startsWith("any-");
117 |
118 | args.add(new QueryPredicateArg.Capture(capture));
119 | args.add(new QueryPredicateArg.Literal(pattern.pattern()));
120 | }
121 |
122 | @Override
123 | boolean test(QueryMatch match) {
124 | var findNodes1 = match.findNodes(capture);
125 | if (findNodes1.isEmpty()) return !isPositive;
126 | Predicate predicate = node -> {
127 | var text = Objects.requireNonNull(node.getText());
128 | return pattern.matcher(text).hasMatch() == isPositive;
129 | };
130 | if (!isAny) return findNodes1.stream().allMatch(predicate);
131 | return findNodes1.stream().anyMatch(predicate);
132 | }
133 | }
134 |
135 | /**
136 | * Handles the following predicates:
137 | * {@code #any-of?}, {@code #not-any-of?}
138 | */
139 | @NullMarked
140 | public static final class AnyOf extends QueryPredicate {
141 | private final String capture;
142 | private final List values;
143 | private final boolean isPositive;
144 |
145 | static final Set NAMES = Set.of("any-of?", "not-any-of?");
146 |
147 | AnyOf(String name, String capture, List values) {
148 | super(name, values.size() + 1);
149 | this.capture = capture;
150 | this.values = List.copyOf(values);
151 | this.isPositive = name.equals("any-of?");
152 |
153 | args.add(new QueryPredicateArg.Capture(capture));
154 | for (var value : this.values) {
155 | args.add(new QueryPredicateArg.Literal(value));
156 | }
157 | }
158 |
159 | @Override
160 | boolean test(QueryMatch match) {
161 | return match.findNodes(capture).stream().noneMatch(node -> {
162 | var text = Objects.requireNonNull(node.getText());
163 | return values.contains(text) != isPositive;
164 | });
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/QueryPredicateArg.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import org.jspecify.annotations.NullMarked;
4 |
5 | /** An argument to a {@link QueryPredicate}. */
6 | @NullMarked
7 | public sealed interface QueryPredicateArg permits QueryPredicateArg.Capture, QueryPredicateArg.Literal {
8 | /** The value of the argument. */
9 | String value();
10 |
11 | /** A capture argument ({@code @value}). */
12 | record Capture(String value) implements QueryPredicateArg {
13 | @Override
14 | public String toString() {
15 | return "@%s".formatted(value);
16 | }
17 | }
18 |
19 | /** A literal string argument ({@code "value"}). */
20 | record Literal(String value) implements QueryPredicateArg {
21 | @Override
22 | public String toString() {
23 | return "\"%s\"".formatted(value);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Range.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import io.github.treesitter.jtreesitter.internal.TSRange;
4 | import java.lang.foreign.MemorySegment;
5 | import java.lang.foreign.SegmentAllocator;
6 | import org.jspecify.annotations.NullMarked;
7 |
8 | /**
9 | * A range of positions in a text document,
10 | * both in terms of bytes and of row-column points.
11 | */
12 | @NullMarked
13 | public record Range(Point startPoint, Point endPoint, @Unsigned int startByte, @Unsigned int endByte) {
14 | static final Range DEFAULT = new Range(Point.MIN, Point.MAX, 0, -1);
15 |
16 | /**
17 | * Creates an instance of a Range record class.
18 | *
19 | * @throws IllegalArgumentException If {@code startPoint > endPoint} or {@code startByte > endByte}.
20 | */
21 | public Range {
22 | if (startPoint.compareTo(endPoint) > 0) {
23 | throw new IllegalArgumentException("Invalid point range: %s to %s".formatted(startPoint, endPoint));
24 | }
25 | if (Integer.compareUnsigned(startByte, endByte) > 0) {
26 | throw new IllegalArgumentException(String.format(
27 | "Invalid byte range: %s to %s",
28 | Integer.toUnsignedString(startByte), Integer.toUnsignedString(endByte)));
29 | }
30 | }
31 |
32 | static Range from(MemorySegment range) {
33 | int endByte = TSRange.end_byte(range), startByte = TSRange.start_byte(range);
34 | MemorySegment startPoint = TSRange.start_point(range), endPoint = TSRange.end_point(range);
35 | return new Range(Point.from(startPoint), Point.from(endPoint), startByte, endByte);
36 | }
37 |
38 | MemorySegment into(SegmentAllocator allocator) {
39 | var range = TSRange.allocate(allocator);
40 | TSRange.start_byte(range, startByte);
41 | TSRange.end_byte(range, endByte);
42 | TSRange.start_point(range, startPoint.into(allocator));
43 | TSRange.end_point(range, endPoint.into(allocator));
44 | return range;
45 | }
46 |
47 | @Override
48 | public String toString() {
49 | return String.format(
50 | "Range[startPoint=%s, endPoint=%s, startByte=%s, endByte=%s]",
51 | startPoint, endPoint, Integer.toUnsignedString(startByte), Integer.toUnsignedString(endByte));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Tree.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import io.github.treesitter.jtreesitter.internal.TSRange;
6 | import io.github.treesitter.jtreesitter.internal.TreeSitter;
7 | import java.lang.foreign.*;
8 | import java.nio.charset.Charset;
9 | import java.util.ArrayList;
10 | import java.util.Collections;
11 | import java.util.List;
12 | import org.jspecify.annotations.NullMarked;
13 | import org.jspecify.annotations.Nullable;
14 |
15 | /** A class that represents a syntax tree. */
16 | @NullMarked
17 | public final class Tree implements AutoCloseable, Cloneable {
18 | private final MemorySegment self;
19 | private byte[] source;
20 | private @Nullable Charset charset;
21 | private final Arena arena;
22 | private final Language language;
23 | private @Nullable List includedRanges;
24 |
25 | Tree(MemorySegment self, Language language, @Nullable String source, @Nullable Charset charset) {
26 | arena = Arena.ofShared();
27 | this.self = self.reinterpret(arena, TreeSitter::ts_tree_delete);
28 | this.language = language;
29 | this.source = source != null && charset != null ? source.getBytes(charset) : new byte[0];
30 | this.charset = charset;
31 | }
32 |
33 | private Tree(Tree tree) {
34 | var copy = ts_tree_copy(tree.self);
35 | arena = Arena.ofShared();
36 | self = copy.reinterpret(arena, TreeSitter::ts_tree_delete);
37 | language = tree.language;
38 | source = tree.source;
39 | charset = tree.charset;
40 | includedRanges = tree.includedRanges;
41 | }
42 |
43 | MemorySegment segment() {
44 | return self;
45 | }
46 |
47 | @Nullable
48 | String getRegion(@Unsigned int start, @Unsigned int end) {
49 | var length = Math.min(end, source.length) - start;
50 | return charset != null ? new String(source, start, length, charset) : null;
51 | }
52 |
53 | /** Get the language that was used to parse the syntax tree. */
54 | public Language getLanguage() {
55 | return language;
56 | }
57 |
58 | /** Get the source code of the syntax tree, if available. */
59 | public @Nullable String getText() {
60 | return charset != null ? new String(source, charset) : null;
61 | }
62 |
63 | /** Get the root node of the syntax tree. */
64 | public Node getRootNode() {
65 | return new Node(ts_tree_root_node(arena, self), this);
66 | }
67 |
68 | /**
69 | * Get the root node of the syntax tree, but with
70 | * its position shifted forward by the given offset.
71 | */
72 | public @Nullable Node getRootNodeWithOffset(@Unsigned int bytes, Point extent) {
73 | try (var alloc = Arena.ofShared()) {
74 | var offsetExtent = extent.into(alloc);
75 | var node = ts_tree_root_node_with_offset(arena, self, bytes, offsetExtent);
76 | if (ts_node_is_null(node)) return null;
77 | return new Node(node, this);
78 | }
79 | }
80 |
81 | /** Get the included ranges of the syntax tree. */
82 | public List getIncludedRanges() {
83 | if (includedRanges == null) {
84 | try (var alloc = Arena.ofConfined()) {
85 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment());
86 | var ranges = ts_tree_included_ranges(self, length);
87 | int size = length.get(C_INT, 0);
88 | if (size == 0) return Collections.emptyList();
89 |
90 | includedRanges = new ArrayList<>(size);
91 | for (int i = 0; i < size; ++i) {
92 | var range = TSRange.asSlice(ranges, i);
93 | includedRanges.add(Range.from(range));
94 | }
95 | free(ranges);
96 | }
97 | }
98 | return Collections.unmodifiableList(includedRanges);
99 | }
100 |
101 | /**
102 | * Compare an old edited syntax tree to a new
103 | * syntax tree representing the same document.
104 | *
105 | * For this to work correctly, this tree must have been
106 | * edited such that its ranges match up to the new tree.
107 | *
108 | * @return A list of ranges whose syntactic structure has changed.
109 | */
110 | public List getChangedRanges(Tree newTree) {
111 | try (var alloc = Arena.ofConfined()) {
112 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment());
113 | var ranges = ts_tree_get_changed_ranges(self, newTree.self, length);
114 | int size = length.get(C_INT, 0);
115 | if (size == 0) return Collections.emptyList();
116 |
117 | var changedRanges = new ArrayList(size);
118 | for (int i = 0; i < size; ++i) {
119 | var range = TSRange.asSlice(ranges, i);
120 | changedRanges.add(Range.from(range));
121 | }
122 | free(ranges);
123 | return changedRanges;
124 | }
125 | }
126 |
127 | /**
128 | * Edit the syntax tree to keep it in sync
129 | * with source code that has been modified.
130 | */
131 | public void edit(InputEdit edit) {
132 | try (var alloc = Arena.ofConfined()) {
133 | ts_tree_edit(self, edit.into(alloc));
134 | } finally {
135 | source = new byte[0];
136 | charset = null;
137 | }
138 | }
139 |
140 | /** Create a new tree cursor starting from the root node of the tree. */
141 | public TreeCursor walk() {
142 | return new TreeCursor(this);
143 | }
144 |
145 | /**
146 | * Create a shallow copy of the syntax tree.
147 | *
148 | * @implNote You need to clone a tree in order to use it on more than
149 | * one thread at a time, as {@linkplain Tree} objects are not thread safe.
150 | */
151 | @Override
152 | @SuppressWarnings("MethodDoesntCallSuperMethod")
153 | public Tree clone() {
154 | return new Tree(this);
155 | }
156 |
157 | @Override
158 | public void close() throws RuntimeException {
159 | arena.close();
160 | }
161 |
162 | @Override
163 | public String toString() {
164 | return "Tree{language=%s, source=%s}".formatted(language, source);
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*;
4 |
5 | import java.lang.foreign.Arena;
6 | import java.lang.foreign.MemorySegment;
7 | import java.lang.foreign.SegmentAllocator;
8 | import java.util.OptionalInt;
9 | import org.jspecify.annotations.NullMarked;
10 | import org.jspecify.annotations.Nullable;
11 |
12 | /**
13 | * A class that can be used to efficiently walk a {@linkplain Tree syntax tree}.
14 | *
15 | * @apiNote The node the cursor was constructed with is considered the
16 | * root of the cursor, and the cursor cannot walk outside this node.
17 | */
18 | @NullMarked
19 | public final class TreeCursor implements AutoCloseable, Cloneable {
20 | private final MemorySegment self;
21 | private final Arena arena;
22 | private final Tree tree;
23 | private @Nullable Node node;
24 |
25 | TreeCursor(Node node, Tree tree) {
26 | arena = Arena.ofShared();
27 | self = ts_tree_cursor_new(arena, node.copy(arena));
28 | this.tree = tree;
29 | }
30 |
31 | TreeCursor(Tree tree) {
32 | arena = Arena.ofShared();
33 | var node = ts_tree_root_node(arena, tree.segment());
34 | self = ts_tree_cursor_new(arena, node);
35 | this.tree = tree;
36 | }
37 |
38 | private TreeCursor(TreeCursor cursor) {
39 | arena = Arena.ofShared();
40 | self = ts_tree_cursor_copy(arena, cursor.self);
41 | tree = cursor.tree.clone();
42 | node = cursor.node;
43 | }
44 |
45 | /**
46 | * Get the current node of the cursor.
47 | *
48 | * @implNote The node will become invalid once the cursor is closed.
49 | */
50 | public Node getCurrentNode() {
51 | if (this.node == null) {
52 | var node = ts_tree_cursor_current_node(arena, self);
53 | this.node = new Node(node, tree);
54 | }
55 | return this.node;
56 | }
57 |
58 | /**
59 | * Get the current node of the cursor using the given allocator.
60 | *
61 | * @since 0.25.0
62 | */
63 | public Node getCurrentNode(SegmentAllocator allocator) {
64 | var node = ts_tree_cursor_current_node(allocator, self);
65 | return new Node(node, tree);
66 | }
67 |
68 | /**
69 | * Get the depth of the cursor's current node relative to
70 | * the original node that the cursor was constructed with.
71 | */
72 | public @Unsigned int getCurrentDepth() {
73 | return ts_tree_cursor_current_depth(self);
74 | }
75 |
76 | /**
77 | * Get the field ID of the tree cursor's current node, or {@code 0}.
78 | *
79 | * @see Node#getChildByFieldId
80 | * @see Language#getFieldIdForName
81 | */
82 | public @Unsigned short getCurrentFieldId() {
83 | return ts_tree_cursor_current_field_id(self);
84 | }
85 |
86 | /**
87 | * Get the field name of the tree cursor's current node, or {@code null}.
88 | *
89 | * @see Node#getChildByFieldName
90 | */
91 | public @Nullable String getCurrentFieldName() {
92 | var segment = ts_tree_cursor_current_field_name(self);
93 | return segment.equals(MemorySegment.NULL) ? null : segment.getString(0);
94 | }
95 |
96 | /**
97 | * Get the index of the cursor's current node out of the descendants
98 | * of the original node that the cursor was constructed with.
99 | */
100 | public @Unsigned int getCurrentDescendantIndex() {
101 | return ts_tree_cursor_current_descendant_index(self);
102 | }
103 |
104 | /**
105 | * Move the cursor to the first child of its current node.
106 | *
107 | * @return {@code true} if the cursor successfully moved, or
108 | * {@code false} if there were no children.
109 | */
110 | public boolean gotoFirstChild() {
111 | var result = ts_tree_cursor_goto_first_child(self);
112 | if (result) node = null;
113 | return result;
114 | }
115 |
116 | /**
117 | * Move the cursor to the last child of its current node.
118 | *
119 | * @return {@code true} if the cursor successfully moved, or
120 | * {@code false} if there were no children.
121 | */
122 | public boolean gotoLastChild() {
123 | var result = ts_tree_cursor_goto_last_child(self);
124 | if (result) node = null;
125 | return result;
126 | }
127 |
128 | /**
129 | * Move the cursor to the parent of its current node.
130 | *
131 | * @return {@code true} if the cursor successfully moved, or
132 | * {@code false} if there was no parent node.
133 | */
134 | public boolean gotoParent() {
135 | var result = ts_tree_cursor_goto_parent(self);
136 | if (result) node = null;
137 | return result;
138 | }
139 |
140 | /**
141 | * Move the cursor to the next sibling of its current node.
142 | *
143 | * @return {@code true} if the cursor successfully moved, or
144 | * {@code false} if there was no next sibling node.
145 | */
146 | public boolean gotoNextSibling() {
147 | var result = ts_tree_cursor_goto_next_sibling(self);
148 | if (result) node = null;
149 | return result;
150 | }
151 |
152 | /**
153 | * Move the cursor to the previous sibling of its current node.
154 | *
155 | * @return {@code true} if the cursor successfully moved, or
156 | * {@code false} if there was no previous sibling node.
157 | */
158 | public boolean gotoPreviousSibling() {
159 | var result = ts_tree_cursor_goto_previous_sibling(self);
160 | if (result) node = null;
161 | return result;
162 | }
163 |
164 | /**
165 | * Move the cursor to the node that is the nth descendant of
166 | * the original node that the cursor was constructed with.
167 | *
168 | * @apiNote The index {@code 0} represents the original node itself.
169 | */
170 | public void gotoDescendant(@Unsigned int index) {
171 | ts_tree_cursor_goto_descendant(self, index);
172 | node = null;
173 | }
174 |
175 | /**
176 | * Move the cursor to the first child of its current node
177 | * that contains or starts after the given byte offset.
178 | *
179 | * @return The index of the child node, if found.
180 | */
181 | public @Unsigned OptionalInt gotoFirstChildForByte(@Unsigned int offset) {
182 | var index = ts_tree_cursor_goto_first_child_for_byte(self, offset);
183 | if (index == -1L) return OptionalInt.empty();
184 | node = null;
185 | return OptionalInt.of((int) index);
186 | }
187 |
188 | /**
189 | * Move the cursor to the first child of its current node
190 | * that contains or starts after the given point.
191 | *
192 | * @return The index of the child node, if found.
193 | */
194 | public @Unsigned OptionalInt gotoFirstChildForPoint(Point point) {
195 | try (var arena = Arena.ofConfined()) {
196 | var goal = point.into(arena);
197 | var index = ts_tree_cursor_goto_first_child_for_point(self, goal);
198 | if (index == -1L) return OptionalInt.empty();
199 | node = null;
200 | return OptionalInt.of((int) index);
201 | }
202 | }
203 |
204 | /** Reset the cursor to start at a different node. */
205 | public void reset(Node node) {
206 | try (var arena = Arena.ofConfined()) {
207 | ts_tree_cursor_reset(self, node.copy(arena));
208 | } finally {
209 | this.node = null;
210 | }
211 | }
212 |
213 | /** Reset the cursor to start at the same position as another cursor. */
214 | public void reset(TreeCursor cursor) {
215 | ts_tree_cursor_reset_to(self, cursor.self);
216 | this.node = null;
217 | }
218 |
219 | /** Create a shallow copy of the tree cursor. */
220 | @Override
221 | @SuppressWarnings("MethodDoesntCallSuperMethod")
222 | public TreeCursor clone() {
223 | return new TreeCursor(this);
224 | }
225 |
226 | @Override
227 | public void close() throws RuntimeException {
228 | ts_tree_cursor_delete(self);
229 | arena.close();
230 | }
231 |
232 | @Override
233 | public String toString() {
234 | return "TreeCursor{tree=%s}".formatted(tree);
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/Unsigned.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import java.lang.annotation.Documented;
4 | import java.lang.annotation.ElementType;
5 | import java.lang.annotation.Target;
6 |
7 | /**
8 | * Specifies that the value is of an unsigned data type.
9 | *
10 | * @see Integer#compareUnsigned
11 | * @see Integer#toUnsignedString
12 | * @see Short#compareUnsigned
13 | * @see Short#toUnsignedInt
14 | */
15 | @Documented
16 | @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
17 | public @interface Unsigned {}
18 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter.internal;
2 |
3 | import io.github.treesitter.jtreesitter.NativeLibraryLookup;
4 | import java.lang.foreign.Arena;
5 | import java.lang.foreign.Linker;
6 | import java.lang.foreign.SymbolLookup;
7 | import java.util.Optional;
8 | import java.util.ServiceLoader;
9 |
10 | @SuppressWarnings("unused")
11 | final class ChainedLibraryLookup implements NativeLibraryLookup {
12 | private ChainedLibraryLookup() {}
13 |
14 | static ChainedLibraryLookup INSTANCE = new ChainedLibraryLookup();
15 |
16 | @Override
17 | public SymbolLookup get(Arena arena) {
18 | var serviceLoader = ServiceLoader.load(NativeLibraryLookup.class);
19 | // NOTE: can't use _ because of palantir/palantir-java-format#934
20 | SymbolLookup lookup = (name) -> Optional.empty();
21 | for (var libraryLookup : serviceLoader) {
22 | lookup = lookup.or(libraryLookup.get(arena));
23 | }
24 | return lookup.or(findLibrary(arena)).or(Linker.nativeLinker().defaultLookup());
25 | }
26 |
27 | private static SymbolLookup findLibrary(Arena arena) {
28 | try {
29 | var library = System.mapLibraryName("tree-sitter");
30 | return SymbolLookup.libraryLookup(library, arena);
31 | } catch (IllegalArgumentException e) {
32 | return SymbolLookup.loaderLookup();
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/io/github/treesitter/jtreesitter/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Java bindings to the tree-sitter parsing library.
3 | *
4 | * Requirements
5 | *
6 | *
17 | *
18 | * Basic Usage
19 | *
20 | * {@snippet lang = java:
21 | * Language language = new Language(TreeSitterJava.language());
22 | * try (Parser parser = new Parser(language)) {
23 | * try (Tree tree = parser.parse("void main() {}", InputEncoding.UTF_8).orElseThrow()) {
24 | * Node rootNode = tree.getRootNode();
25 | * assert rootNode.getType().equals("program");
26 | * assert rootNode.getStartPoint().column() == 0;
27 | * assert rootNode.getEndPoint().column() == 14;
28 | * }
29 | * }
30 | *}
31 | *
32 | * Library Loading
33 | *
34 | * There are three ways to load the shared libraries:
35 | *
36 | *
37 | * -
38 | * The libraries can be installed in the OS-specific library search path or in
39 | * {@systemProperty java.library.path}. The search path can be amended using the
40 | * {@code LD_LIBRARY_PATH} environment variable on Linux, {@code DYLD_LIBRARY_PATH}
41 | * on macOS, or {@code PATH} on Windows. The libraries will be loaded automatically by
42 | * {@link java.lang.foreign.SymbolLookup#libraryLookup(String, java.lang.foreign.Arena)
43 | * SymbolLookup.libraryLookup(String, Arena)}.
44 | *
45 | * -
46 | * The libraries can be loaded manually by calling
47 | * {@link java.lang.System#loadLibrary(String) System.loadLibrary(String)},
48 | * if the library is installed in {@systemProperty java.library.path},
49 | * or {@link java.lang.System#load(String) System.load(String)}.
50 | *
51 | * -
52 | * The libraries can be loaded manually by registering a custom implementation of
53 | * {@link io.github.treesitter.jtreesitter.NativeLibraryLookup NativeLibraryLookup}.
54 | * This can be used, for example, to load libraries from inside a JAR file.
55 | *
56 | *
57 | */
58 | package io.github.treesitter.jtreesitter;
59 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/LanguageTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import org.junit.jupiter.api.BeforeAll;
7 | import org.junit.jupiter.api.Test;
8 |
9 | public class LanguageTest {
10 | private static Language language;
11 |
12 | @BeforeAll
13 | static void beforeAll() {
14 | language = new Language(TreeSitterJava.language());
15 | }
16 |
17 | @Test
18 | void getAbiVersion() {
19 | assertEquals(14, language.getAbiVersion());
20 | }
21 |
22 | @Test
23 | void getName() {
24 | assertNull(language.getName());
25 | }
26 |
27 | @Test
28 | void getMetadata() {
29 | assertNull(language.getMetadata());
30 | }
31 |
32 | @Test
33 | void getSymbolCount() {
34 | assertEquals(321, language.getSymbolCount());
35 | }
36 |
37 | @Test
38 | void getStateCount() {
39 | assertEquals(1385, language.getStateCount());
40 | }
41 |
42 | @Test
43 | void getFieldCount() {
44 | assertEquals(40, language.getFieldCount());
45 | }
46 |
47 | @Test
48 | void getSymbolName() {
49 | assertEquals("identifier", language.getSymbolName((short) 1));
50 | assertNull(language.getSymbolName((short) 999));
51 | }
52 |
53 | @Test
54 | void getSymbolForName() {
55 | assertEquals((short) 138, language.getSymbolForName("program", true));
56 | assertEquals((short) 0, language.getSymbolForName("$", false));
57 | }
58 |
59 | @Test
60 | void getSupertypes() {
61 | assertArrayEquals(new short[0], language.getSupertypes());
62 | }
63 |
64 | @Test
65 | void getSubtypes() {
66 | assertArrayEquals(new short[0], language.getSubtypes((short) 1));
67 | }
68 |
69 | @Test
70 | void isNamed() {
71 | assertTrue(language.isNamed((short) 1));
72 | }
73 |
74 | @Test
75 | void isVisible() {
76 | assertTrue(language.isVisible((short) 1));
77 | }
78 |
79 | @Test
80 | void isSupertype() {
81 | assertFalse(language.isSupertype((short) 1));
82 | }
83 |
84 | @Test
85 | void getFieldNameForId() {
86 | assertNotNull(language.getFieldNameForId((short) 20));
87 | }
88 |
89 | @Test
90 | void getFieldIdForName() {
91 | assertEquals(20, language.getFieldIdForName("name"));
92 | }
93 |
94 | @Test
95 | void nextState() {
96 | assertNotEquals(0, language.nextState((short) 1, (short) 138));
97 | }
98 |
99 | @Test
100 | void lookaheadIterator() {
101 | assertDoesNotThrow(() -> {
102 | var state = language.nextState((short) 1, (short) 138);
103 | language.lookaheadIterator(state).close();
104 | });
105 | }
106 |
107 | @Test
108 | void testEquals() {
109 | var other = new Language(TreeSitterJava.language());
110 | assertEquals(other, language.clone());
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/LookaheadIteratorTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import java.util.List;
7 | import org.junit.jupiter.api.*;
8 |
9 | class LookaheadIteratorTest {
10 | private static Language language;
11 | private static short state;
12 | private LookaheadIterator lookahead;
13 |
14 | @BeforeAll
15 | static void beforeAll() {
16 | language = new Language(TreeSitterJava.language());
17 | state = language.nextState((short) 1, (short) 138);
18 | }
19 |
20 | @BeforeEach
21 | void setUp() {
22 | lookahead = language.lookaheadIterator(state);
23 | }
24 |
25 | @AfterEach
26 | void tearDown() {
27 | lookahead.close();
28 | }
29 |
30 | @Test
31 | void getLanguage() {
32 | assertEquals(language, lookahead.getLanguage());
33 | }
34 |
35 | @Test
36 | void getCurrentSymbol() {
37 | assertEquals((short) -1, lookahead.getCurrentSymbol());
38 | }
39 |
40 | @Test
41 | void getCurrentSymbolName() {
42 | assertEquals("ERROR", lookahead.getCurrentSymbolName());
43 | }
44 |
45 | @Test
46 | @DisplayName("reset(state)")
47 | void resetState() {
48 | assertDoesNotThrow(() -> lookahead.next());
49 | assertTrue(lookahead.reset(state));
50 | assertEquals("ERROR", lookahead.getCurrentSymbolName());
51 | }
52 |
53 | @Test
54 | @DisplayName("reset(language)")
55 | void resetLanguage() {
56 | assertDoesNotThrow(() -> lookahead.next());
57 | assertTrue(lookahead.reset(state, language));
58 | assertEquals("ERROR", lookahead.getCurrentSymbolName());
59 | }
60 |
61 | @Test
62 | void hasNext() {
63 | assertTrue(lookahead.hasNext());
64 | assertEquals("ERROR", lookahead.getCurrentSymbolName());
65 | }
66 |
67 | @Test
68 | void next() {
69 | assertEquals("end", lookahead.next().name());
70 | }
71 |
72 | @Test
73 | void symbols() {
74 | assertEquals(3, lookahead.symbols().count());
75 | }
76 |
77 | @Test
78 | void names() {
79 | var names = List.of("end", "line_comment", "block_comment");
80 | assertEquals(names, lookahead.names().toList());
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/NodeTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import org.junit.jupiter.api.*;
7 |
8 | class NodeTest {
9 | private static Tree tree;
10 | private static Node node;
11 |
12 | @BeforeAll
13 | static void beforeAll() {
14 | var language = new Language(TreeSitterJava.language());
15 | try (var parser = new Parser(language)) {
16 | tree = parser.parse("class Foo {} // uni©ode").orElseThrow();
17 | node = tree.getRootNode();
18 | }
19 | }
20 |
21 | @AfterAll
22 | static void afterAll() {
23 | tree.close();
24 | }
25 |
26 | @Test
27 | void getTree() {
28 | assertSame(tree, node.getTree());
29 | }
30 |
31 | @Test
32 | void getId() {
33 | assertNotEquals(0L, node.getId());
34 | }
35 |
36 | @Test
37 | void getSymbol() {
38 | assertEquals(138, node.getSymbol());
39 | }
40 |
41 | @Test
42 | void getGrammarSymbol() {
43 | assertEquals(138, node.getGrammarSymbol());
44 | }
45 |
46 | @Test
47 | void getType() {
48 | assertEquals("program", node.getType());
49 | }
50 |
51 | @Test
52 | void getGrammarType() {
53 | assertEquals("program", node.getGrammarType());
54 | }
55 |
56 | @Test
57 | void isNamed() {
58 | assertTrue(node.isNamed());
59 | }
60 |
61 | @Test
62 | void isExtra() {
63 | assertFalse(node.isExtra());
64 | }
65 |
66 | @Test
67 | void isError() {
68 | assertFalse(node.isError());
69 | }
70 |
71 | @Test
72 | void isMissing() {
73 | assertFalse(node.isMissing());
74 | }
75 |
76 | @Test
77 | void hasChanges() {
78 | assertFalse(node.hasChanges());
79 | }
80 |
81 | @Test
82 | void hasError() {
83 | assertFalse(node.hasError());
84 | }
85 |
86 | @Test
87 | void getParseState() {
88 | assertEquals(0, node.getParseState());
89 | }
90 |
91 | @Test
92 | void getNextParseState() {
93 | assertEquals(0, node.getNextParseState());
94 | }
95 |
96 | @Test
97 | void getStartByte() {
98 | assertEquals(0, node.getStartByte());
99 | }
100 |
101 | @Test
102 | void getEndByte() {
103 | assertEquals(24, node.getEndByte());
104 | }
105 |
106 | @Test
107 | void getRange() {
108 | Point startPoint = new Point(0, 0), endPoint = new Point(0, 24);
109 | assertEquals(new Range(startPoint, endPoint, 0, 24), node.getRange());
110 | }
111 |
112 | @Test
113 | void getStartPoint() {
114 | assertEquals(new Point(0, 0), node.getStartPoint());
115 | }
116 |
117 | @Test
118 | void getEndPoint() {
119 | assertEquals(new Point(0, 24), node.getEndPoint());
120 | }
121 |
122 | @Test
123 | void getChildCount() {
124 | assertEquals(2, node.getChildCount());
125 | }
126 |
127 | @Test
128 | void getNamedChildCount() {
129 | assertEquals(2, node.getNamedChildCount());
130 | }
131 |
132 | @Test
133 | void getDescendantCount() {
134 | assertEquals(8, node.getDescendantCount());
135 | }
136 |
137 | @Test
138 | void getParent() {
139 | assertTrue(node.getParent().isEmpty());
140 | }
141 |
142 | @Test
143 | void getNextSibling() {
144 | assertTrue(node.getNextSibling().isEmpty());
145 | }
146 |
147 | @Test
148 | void getPrevSibling() {
149 | assertTrue(node.getPrevSibling().isEmpty());
150 | }
151 |
152 | @Test
153 | void getNextNamedSibling() {
154 | assertTrue(node.getNextNamedSibling().isEmpty());
155 | }
156 |
157 | @Test
158 | void getPrevNamedSibling() {
159 | assertTrue(node.getPrevNamedSibling().isEmpty());
160 | }
161 |
162 | @Test
163 | void getChild() {
164 | var child = node.getChild(0).orElseThrow();
165 | assertEquals("class_declaration", child.getType());
166 | }
167 |
168 | @Test
169 | void getNamedChild() {
170 | var child = node.getNamedChild(0).orElseThrow();
171 | assertEquals("class_declaration", child.getGrammarType());
172 | }
173 |
174 | @Test
175 | void getFirstChildForByte() {
176 | var child = node.getFirstChildForByte(15).orElseThrow();
177 | assertEquals("line_comment", child.getGrammarType());
178 | }
179 |
180 | @Test
181 | void getFirstNamedChildForByte() {
182 | var child = node.getFirstNamedChildForByte(15).orElseThrow();
183 | assertEquals("line_comment", child.getGrammarType());
184 | }
185 |
186 | @Test
187 | void getChildByFieldId() {
188 | var child = node.getChild(0).orElseThrow();
189 | child = child.getChildByFieldId((short) 20).orElseThrow();
190 | assertEquals("identifier", child.getType());
191 | }
192 |
193 | @Test
194 | void getChildByFieldName() {
195 | var child = node.getChild(0).orElseThrow();
196 | child = child.getChildByFieldName("name").orElseThrow();
197 | assertEquals("identifier", child.getGrammarType());
198 | }
199 |
200 | @Test
201 | void getChildren() {
202 | var children = node.getChild(0).orElseThrow().getChildren();
203 | assertEquals(3, children.size());
204 | assertEquals("class", children.getFirst().getType());
205 | }
206 |
207 | @Test
208 | void getNamedChildren() {
209 | var children = node.getChild(0).orElseThrow().getNamedChildren();
210 | assertEquals(2, children.size());
211 | assertEquals("identifier", children.getFirst().getType());
212 | }
213 |
214 | @Test
215 | void getChildrenByFieldId() {
216 | var children = node.getChild(0).orElseThrow().getChildrenByFieldId((short) 1);
217 | assertTrue(children.isEmpty());
218 | }
219 |
220 | @Test
221 | void getChildrenByFieldName() {
222 | var children = node.getChild(0).orElseThrow().getChildrenByFieldName("body");
223 | assertEquals(1, children.size());
224 | assertEquals("class_body", children.getFirst().getType());
225 | }
226 |
227 | @Test
228 | void getFieldNameForChild() {
229 | var child = node.getChild(0).orElseThrow();
230 | assertNull(child.getFieldNameForChild(0));
231 | assertEquals("body", child.getFieldNameForChild(2));
232 | }
233 |
234 | @Test
235 | void getFieldNameForNamedChild() {
236 | var child = node.getChild(0).orElseThrow();
237 | assertNull(child.getFieldNameForNamedChild(2));
238 | }
239 |
240 | @Test
241 | @DisplayName("getDescendant(bytes)")
242 | void getDescendantBytes() {
243 | var descendant = node.getDescendant(0, 5).orElseThrow();
244 | assertEquals("class", descendant.getType());
245 | }
246 |
247 | @Test
248 | @DisplayName("getDescendant(points)")
249 | void getDescendantPoints() {
250 | Point startPoint = new Point(0, 10), endPoint = new Point(0, 12);
251 | var descendant = node.getDescendant(startPoint, endPoint).orElseThrow();
252 | assertEquals("class_body", descendant.getGrammarType());
253 | }
254 |
255 | @Test
256 | @DisplayName("getNamedDescendant(bytes)")
257 | void getNamedDescendantBytes() {
258 | var descendant = node.getNamedDescendant(0, 5).orElseThrow();
259 | assertEquals("class_declaration", descendant.getType());
260 | }
261 |
262 | @Test
263 | @DisplayName("getNamedDescendant(points)")
264 | void getNamedDescendantPoints() {
265 | Point startPoint = new Point(0, 6), endPoint = new Point(0, 9);
266 | var descendant = node.getNamedDescendant(startPoint, endPoint).orElseThrow();
267 | assertEquals("identifier", descendant.getGrammarType());
268 | }
269 |
270 | @Test
271 | void getChildWithDescendant() {
272 | var descendant = node.getChild(0).orElseThrow();
273 | var child = node.getChildWithDescendant(descendant);
274 | assertEquals("class_declaration", child.orElseThrow().getType());
275 | }
276 |
277 | @Test
278 | void getText() {
279 | var child = node.getChild(1).orElseThrow();
280 | assertEquals("// uni©ode", child.getText());
281 | }
282 |
283 | @Test
284 | void edit() {
285 | var edit = new InputEdit(0, 12, 10, new Point(0, 0), new Point(0, 12), new Point(0, 10));
286 | try (var copy = tree.clone()) {
287 | var node = copy.getRootNode();
288 | copy.edit(edit);
289 | node.edit(edit);
290 | assertTrue(node.hasChanges());
291 | }
292 | }
293 |
294 | @Test
295 | void walk() {
296 | var child = node.getChild(0).orElseThrow();
297 | try (var cursor = child.walk()) {
298 | assertEquals(child, cursor.getCurrentNode());
299 | }
300 | }
301 |
302 | @Test
303 | void toSexp() {
304 | var sexp = "(program (class_declaration name: (identifier) body: (class_body)) (line_comment))";
305 | assertEquals(sexp, node.toSexp());
306 | }
307 |
308 | @Test
309 | void equals() {
310 | var other = node.getChild(0).orElseThrow();
311 | assertNotEquals(node, other);
312 | other = other.getParent().orElseThrow();
313 | assertEquals(node, other);
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/ParserTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import java.nio.charset.StandardCharsets;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import java.util.concurrent.*;
10 | import org.junit.jupiter.api.*;
11 |
12 | class ParserTest {
13 | private static Language language;
14 | private Parser parser;
15 |
16 | @BeforeAll
17 | static void beforeAll() {
18 | language = new Language(TreeSitterJava.language());
19 | }
20 |
21 | @BeforeEach
22 | void setUp() {
23 | parser = new Parser();
24 | }
25 |
26 | @AfterEach
27 | void tearDown() {
28 | parser.close();
29 | }
30 |
31 | @Test
32 | void getLanguage() {
33 | assertNull(parser.getLanguage());
34 | }
35 |
36 | @Test
37 | void setLanguage() {
38 | assertSame(parser, parser.setLanguage(language));
39 | assertEquals(language, parser.getLanguage());
40 | }
41 |
42 | @Test
43 | void setLogger() {
44 | assertSame(parser, parser.setLogger(null));
45 | }
46 |
47 | @Test
48 | void getIncludedRanges() {
49 | assertEquals(1, parser.getIncludedRanges().size());
50 | }
51 |
52 | @Test
53 | void setIncludedRanges() {
54 | var range = new Range(Point.MIN, new Point(0, 1), 0, 1);
55 | assertSame(parser, parser.setIncludedRanges(List.of(range)));
56 | assertIterableEquals(List.of(range), parser.getIncludedRanges());
57 | assertThrows(IllegalArgumentException.class, () -> parser.setIncludedRanges(List.of(range, range)));
58 | }
59 |
60 | @Test
61 | @DisplayName("parse(utf8)")
62 | void parseUtf8() {
63 | parser.setLanguage(language);
64 | try (var tree = parser.parse("class Foo {}").orElseThrow()) {
65 | var rootNode = tree.getRootNode();
66 |
67 | assertEquals(12, rootNode.getEndByte());
68 | assertFalse(rootNode.isError());
69 | assertEquals("(program (class_declaration name: (identifier) body: (class_body)))", rootNode.toSexp());
70 | }
71 | }
72 |
73 | @Test
74 | @DisplayName("parse(utf16)")
75 | void parseUtf16() {
76 | parser.setLanguage(language);
77 | var encoding = InputEncoding.valueOf(StandardCharsets.UTF_16);
78 | try (var tree = parser.parse("var java = \"💩\";", encoding).orElseThrow()) {
79 | var rootNode = tree.getRootNode();
80 |
81 | assertEquals(32, rootNode.getEndByte());
82 | assertFalse(rootNode.isError());
83 | assertEquals(
84 | "(program (local_variable_declaration type: (type_identifier) declarator: (variable_declarator name: (identifier) value: (string_literal (string_fragment)))))",
85 | rootNode.toSexp());
86 | }
87 | }
88 |
89 | @Test
90 | @DisplayName("parse(logger)")
91 | void parseLogger() {
92 | var messages = new ArrayList();
93 | parser.setLanguage(language)
94 | .setLogger((type, message) -> messages.add("%s - %s".formatted(type.name(), message)))
95 | .parse("class Foo {}")
96 | .orElseThrow()
97 | .close();
98 | assertEquals(44, messages.size());
99 | assertEquals("LEX - new_parse", messages.getFirst());
100 | assertEquals("PARSE - consume character:'c'", messages.get(3));
101 | assertEquals("LEX - done", messages.getLast());
102 | }
103 |
104 | @SuppressWarnings("unused")
105 | @Test
106 | @DisplayName("parse(callback)")
107 | void parseCallback() {
108 | var source = "class Foo {}";
109 | // NOTE: can't use _ because of palantir/palantir-java-format#934
110 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(offset, source.length()));
111 | parser.setLanguage(language);
112 | try (var tree = parser.parse(callback, InputEncoding.UTF_8).orElseThrow()) {
113 | assertNull(tree.getText());
114 | assertEquals("program", tree.getRootNode().getType());
115 | }
116 | }
117 |
118 | @Test
119 | @DisplayName("parse(timeout)")
120 | @SuppressWarnings("deprecation")
121 | void parseTimeout() {
122 | var source = "}".repeat(1024);
123 | // NOTE: can't use _ because of palantir/palantir-java-format#934
124 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1));
125 |
126 | parser.setLanguage(language).setTimeoutMicros(2L);
127 | assertTrue(parser.parse(callback, InputEncoding.UTF_8).isEmpty());
128 | }
129 |
130 | @Test
131 | @DisplayName("parse(cancellation)")
132 | @SuppressWarnings("deprecation")
133 | void parseCancellation() {
134 | var source = "}".repeat(1024 * 1024);
135 | // NOTE: can't use _ because of palantir/palantir-java-format#934
136 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1));
137 |
138 | var flag = new Parser.CancellationFlag();
139 | parser.setLanguage(language).setCancellationFlag(flag);
140 | try (var service = Executors.newFixedThreadPool(2)) {
141 | service.submit(() -> {
142 | try {
143 | wait(10L);
144 | } catch (InterruptedException e) {
145 | service.shutdownNow();
146 | } finally {
147 | flag.set(1L);
148 | }
149 | });
150 | var result = service.submit(() -> parser.parse(callback, InputEncoding.UTF_8));
151 | assertTrue(result.get(30L, TimeUnit.MILLISECONDS).isEmpty());
152 | } catch (InterruptedException | CancellationException | ExecutionException | TimeoutException e) {
153 | fail("Parsing was not halted gracefully", e);
154 | }
155 | }
156 |
157 | @Test
158 | @DisplayName("parse(options)")
159 | void parseOptions() {
160 | var source = "}".repeat(1024);
161 | // NOTE: can't use _ because of palantir/palantir-java-format#934
162 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1));
163 | var options = new Parser.Options((state) -> state.getCurrentByteOffset() >= 1000);
164 |
165 | parser.setLanguage(language);
166 | assertTrue(parser.parse(callback, InputEncoding.UTF_8, options).isEmpty());
167 | }
168 |
169 | @Test
170 | void reset() {
171 | var source = "class foo bar() {}";
172 | // NOTE: can't use _ because of palantir/palantir-java-format#934
173 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1));
174 | var options = new Parser.Options(Parser.State::hasError);
175 |
176 | parser.setLanguage(language);
177 | parser.parse(callback, InputEncoding.UTF_8, options);
178 | parser.reset();
179 | try (var tree = parser.parse("String foo;").orElseThrow()) {
180 | assertFalse(tree.getRootNode().hasError());
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import java.util.function.Consumer;
7 | import org.junit.jupiter.api.AfterAll;
8 | import org.junit.jupiter.api.BeforeAll;
9 | import org.junit.jupiter.api.Test;
10 |
11 | public class QueryCursorTest {
12 | private static Language language;
13 | private static Parser parser;
14 | private static final String source =
15 | """
16 | (identifier) @identifier
17 |
18 | (class_declaration
19 | name: (identifier) @class
20 | (class_body) @body)
21 | """
22 | .stripIndent();
23 |
24 | @BeforeAll
25 | static void beforeAll() {
26 | language = new Language(TreeSitterJava.language());
27 | parser = new Parser(language);
28 | }
29 |
30 | @AfterAll
31 | static void afterAll() {
32 | parser.close();
33 | }
34 |
35 | private static void assertCursor(Consumer assertions) {
36 | assertCursor(source, assertions);
37 | }
38 |
39 | private static void assertCursor(String source, Consumer assertions) {
40 | try (var query = new Query(language, source)) {
41 | try (var cursor = new QueryCursor(query)) {
42 | assertions.accept(cursor);
43 | }
44 | }
45 | }
46 |
47 | @Test
48 | void getMatchLimit() {
49 | assertCursor(cursor -> assertEquals(-1, cursor.getMatchLimit()));
50 | }
51 |
52 | @Test
53 | void setMatchLimit() {
54 | assertCursor(cursor -> {
55 | assertSame(cursor, cursor.setMatchLimit(10));
56 | assertEquals(10, cursor.getMatchLimit());
57 | });
58 | }
59 |
60 | @Test
61 | void setMaxStartDepth() {
62 | assertCursor(cursor -> assertSame(cursor, cursor.setMaxStartDepth(10)));
63 | }
64 |
65 | @Test
66 | void setByteRange() {
67 | assertCursor(cursor -> assertSame(cursor, cursor.setByteRange(1, 10)));
68 | }
69 |
70 | @Test
71 | void setPointRange() {
72 | assertCursor(cursor -> {
73 | Point start = new Point(0, 1), end = new Point(1, 10);
74 | assertSame(cursor, cursor.setPointRange(start, end));
75 | });
76 | }
77 |
78 | @Test
79 | void didExceedMatchLimit() {
80 | assertCursor(cursor -> assertFalse(cursor.didExceedMatchLimit()));
81 | }
82 |
83 | @Test
84 | void findCaptures() {
85 | try (var tree = parser.parse("class Foo {}").orElseThrow()) {
86 | assertCursor(cursor -> {
87 | var matches = cursor.findCaptures(tree.getRootNode()).toList();
88 | assertEquals(3, matches.size());
89 | assertEquals(0, matches.get(0).getKey());
90 | assertEquals(0, matches.get(1).getKey());
91 | assertNotEquals(matches.get(0).getValue(), matches.get(1).getValue());
92 | });
93 | }
94 | }
95 |
96 | @Test
97 | void findMatches() {
98 | try (var tree = parser.parse("class Foo {}").orElseThrow()) {
99 | assertCursor(cursor -> {
100 | var matches = cursor.findMatches(tree.getRootNode()).toList();
101 | assertEquals(2, matches.size());
102 | assertEquals(0, matches.getFirst().patternIndex());
103 | assertEquals(1, matches.getLast().patternIndex());
104 | });
105 | }
106 |
107 | try (var tree = parser.parse("int y = x + 1;").orElseThrow()) {
108 | var source =
109 | """
110 | ((variable_declarator
111 | (identifier) @y
112 | (binary_expression
113 | (identifier) @x))
114 | (#not-eq? @y @x))
115 | """
116 | .stripIndent();
117 | assertCursor(source, cursor -> {
118 | var matches = cursor.findMatches(tree.getRootNode()).toList();
119 | assertEquals(1, matches.size());
120 | assertEquals(
121 | "y", matches.getFirst().captures().getFirst().node().getText());
122 | });
123 | }
124 |
125 | try (var tree = parser.parse("class Foo{}\nclass Bar {}").orElseThrow()) {
126 | var source = """
127 | ((identifier) @foo
128 | (#eq? @foo "Foo"))
129 | """
130 | .stripIndent();
131 | assertCursor(source, cursor -> {
132 | var matches = cursor.findMatches(tree.getRootNode()).toList();
133 | assertEquals(1, matches.size());
134 | assertEquals(
135 | "Foo", matches.getFirst().captures().getFirst().node().getText());
136 | });
137 |
138 | source = """
139 | ((identifier) @name
140 | (#not-any-of? @name "Foo" "Bar"))
141 | """
142 | .stripIndent();
143 | assertCursor(source, cursor -> {
144 | var matches = cursor.findMatches(tree.getRootNode()).toList();
145 | assertTrue(matches.isEmpty());
146 | });
147 |
148 | source = """
149 | ((identifier) @foo
150 | (#ieq? @foo "foo"))
151 | """
152 | .stripIndent();
153 | assertCursor(source, cursor -> {
154 | var options = new QueryCursor.Options((predicate, match) -> {
155 | if (!predicate.getName().equals("ieq?")) return true;
156 | var args = predicate.getArgs();
157 | var node = match.findNodes(args.getFirst().value()).getFirst();
158 | return args.getLast().value().equalsIgnoreCase(node.getText());
159 | });
160 | var matches = cursor.findMatches(tree.getRootNode(), options).toList();
161 | assertEquals(1, matches.size());
162 | assertEquals(
163 | "Foo", matches.getFirst().captures().getFirst().node().getText());
164 | });
165 | }
166 |
167 | // Verify that capture count is treated as `uint16_t` and not as signed Java `short`
168 | try (var tree = parser.parse(";".repeat(Short.MAX_VALUE + 1)).orElseThrow()) {
169 | var source = """
170 | ";"+ @capture
171 | """;
172 | assertCursor(source, cursor -> {
173 | var matches = cursor.findMatches(tree.getRootNode()).toList();
174 | assertEquals(1, matches.size());
175 | assertEquals(Short.MAX_VALUE + 1, matches.getFirst().captures().size());
176 | });
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import java.util.List;
7 | import java.util.NoSuchElementException;
8 | import java.util.function.Consumer;
9 | import org.junit.jupiter.api.AfterAll;
10 | import org.junit.jupiter.api.BeforeAll;
11 | import org.junit.jupiter.api.Test;
12 |
13 | class QueryTest {
14 | private static Language language;
15 | private static Parser parser;
16 | private static final String source =
17 | """
18 | (identifier) @identifier
19 |
20 | (class_declaration
21 | name: (identifier) @class
22 | (class_body) @body)
23 | """
24 | .stripIndent();
25 |
26 | @BeforeAll
27 | static void beforeAll() {
28 | language = new Language(TreeSitterJava.language());
29 | parser = new Parser(language);
30 | }
31 |
32 | @AfterAll
33 | static void afterAll() {
34 | parser.close();
35 | }
36 |
37 | @SuppressWarnings("unused")
38 | private static void assertError(Class extends QueryError> type, String source, String message) {
39 | // NOTE: can't use _ because of palantir/palantir-java-format#934
40 | try (var q = new Query(language, source)) {
41 | fail("Expected QueryError to be thrown, but nothing was thrown.");
42 | } catch (QueryError ex) {
43 | assertInstanceOf(type, ex);
44 | assertEquals(message, ex.getMessage());
45 | }
46 | }
47 |
48 | private static void assertQuery(Consumer assertions) {
49 | assertQuery(source, assertions);
50 | }
51 |
52 | private static void assertQuery(String source, Consumer assertions) {
53 | try (var query = new Query(language, source)) {
54 | assertions.accept(query);
55 | } catch (QueryError e) {
56 | fail("Unexpected query error", e);
57 | }
58 | }
59 |
60 | @Test
61 | void errors() {
62 | assertError(QueryError.Syntax.class, "(identifier) @", "Unexpected EOF");
63 | assertError(QueryError.Syntax.class, " identifier)", "Invalid syntax at row 0, column 1");
64 |
65 | assertError(
66 | QueryError.Capture.class,
67 | "((identifier) @foo\n (#test? @bar))",
68 | "Invalid capture name at row 1, column 10: bar");
69 |
70 | assertError(QueryError.NodeType.class, "(foo)", "Invalid node type at row 0, column 1: foo");
71 |
72 | assertError(QueryError.Field.class, "foo: (identifier)", "Invalid field name at row 0, column 0: foo");
73 |
74 | assertError(QueryError.Structure.class, "(program (identifier))", "Impossible pattern at row 0, column 9");
75 |
76 | assertError(
77 | QueryError.Predicate.class,
78 | "\n((identifier) @foo\n (#any-of?))",
79 | "Invalid predicate in pattern at row 1: #any-of? expects at least 2 arguments, got 0");
80 | }
81 |
82 | @Test
83 | void getPatternCount() {
84 | assertQuery(query -> assertEquals(2, query.getPatternCount()));
85 | }
86 |
87 | @Test
88 | void getCaptureNames() {
89 | assertQuery(query -> assertIterableEquals(List.of("identifier", "class", "body"), query.getCaptureNames()));
90 | }
91 |
92 | @Test
93 | void getStringValues() {
94 | var source = """
95 | ((identifier) @foo
96 | (#eq? @foo "Foo"))
97 | """
98 | .stripIndent();
99 | assertQuery(source, query -> assertIterableEquals(List.of("eq?", "Foo"), query.getStringValues()));
100 | }
101 |
102 | @Test
103 | void disablePattern() {
104 | assertQuery(query -> {
105 | assertDoesNotThrow(() -> query.disablePattern(1));
106 | assertThrows(IndexOutOfBoundsException.class, () -> query.disablePattern(2));
107 | });
108 | }
109 |
110 | @Test
111 | void disableCapture() {
112 | assertQuery(query -> {
113 | assertDoesNotThrow(() -> query.disableCapture("body"));
114 | assertThrows(NoSuchElementException.class, () -> query.disableCapture("none"));
115 | });
116 | }
117 |
118 | @Test
119 | void startByteForPattern() {
120 | assertQuery(query -> {
121 | assertEquals(26, query.startByteForPattern(1));
122 | assertThrows(IndexOutOfBoundsException.class, () -> query.startByteForPattern(2));
123 | });
124 | }
125 |
126 | @Test
127 | void endByteForPattern() {
128 | assertQuery(query -> {
129 | assertEquals(26, query.endByteForPattern(0));
130 | assertThrows(IndexOutOfBoundsException.class, () -> query.endByteForPattern(2));
131 | });
132 | }
133 |
134 | @Test
135 | void isPatternRooted() {
136 | assertQuery(query -> {
137 | assertTrue(query.isPatternRooted(0));
138 | assertThrows(IndexOutOfBoundsException.class, () -> query.isPatternRooted(2));
139 | });
140 | }
141 |
142 | @Test
143 | void isPatternNonLocal() {
144 | assertQuery(query -> {
145 | assertFalse(query.isPatternNonLocal(0));
146 | assertThrows(IndexOutOfBoundsException.class, () -> query.isPatternNonLocal(2));
147 | });
148 | }
149 |
150 | @Test
151 | void isPatternGuaranteedAtStep() {
152 | assertQuery(query -> {
153 | assertFalse(query.isPatternGuaranteedAtStep(27));
154 | assertThrows(IndexOutOfBoundsException.class, () -> query.isPatternGuaranteedAtStep(99));
155 | });
156 | }
157 |
158 | @Test
159 | void getPatternSettings() {
160 | assertQuery("((identifier) @foo (#set! foo))", query -> {
161 | var settings = query.getPatternSettings(0);
162 | assertTrue(settings.get("foo").isEmpty());
163 | });
164 | assertQuery("((identifier) @foo (#set! foo \"FOO\"))", query -> {
165 | var settings = query.getPatternSettings(0);
166 | assertEquals("FOO", settings.get("foo").orElse(null));
167 | });
168 | }
169 |
170 | @Test
171 | void getPatternAssertions() {
172 | assertQuery("((identifier) @foo (#is? foo))", query -> {
173 | var assertions = query.getPatternAssertions(0, true);
174 | assertTrue(assertions.get("foo").isEmpty());
175 | });
176 | assertQuery("((identifier) @foo (#is-not? foo \"FOO\"))", query -> {
177 | var assertions = query.getPatternAssertions(0, false);
178 | assertEquals("FOO", assertions.get("foo").orElse(null));
179 | });
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import java.lang.foreign.Arena;
7 | import org.junit.jupiter.api.AfterAll;
8 | import org.junit.jupiter.api.AfterEach;
9 | import org.junit.jupiter.api.BeforeAll;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.DisplayName;
12 | import org.junit.jupiter.api.Test;
13 |
14 | class TreeCursorTest {
15 | private static Tree tree;
16 | private TreeCursor cursor;
17 |
18 | @BeforeAll
19 | static void beforeAll() {
20 | var language = new Language(TreeSitterJava.language());
21 | try (var parser = new Parser(language)) {
22 | tree = parser.parse("class Foo {}").orElseThrow();
23 | }
24 | }
25 |
26 | @AfterAll
27 | static void afterAll() {
28 | tree.close();
29 | }
30 |
31 | @BeforeEach
32 | void setUp() {
33 | cursor = tree.walk();
34 | }
35 |
36 | @AfterEach
37 | void tearDown() {
38 | cursor.close();
39 | }
40 |
41 | @Test
42 | void getCurrentNode() {
43 | var node = cursor.getCurrentNode();
44 | assertEquals(tree.getRootNode(), node);
45 | assertSame(node, cursor.getCurrentNode());
46 |
47 | try (var arena = Arena.ofConfined()) {
48 | try (var copy = cursor.clone()) {
49 | node = copy.getCurrentNode(arena);
50 | assertEquals(node, tree.getRootNode());
51 | }
52 | // can still access node after cursor was closed
53 | assertEquals(node, tree.getRootNode());
54 | }
55 | }
56 |
57 | @Test
58 | void getCurrentDepth() {
59 | assertEquals(0, cursor.getCurrentDepth());
60 | }
61 |
62 | @Test
63 | void getCurrentFieldId() {
64 | assertEquals(0, cursor.getCurrentFieldId());
65 | }
66 |
67 | @Test
68 | void getCurrentFieldName() {
69 | assertNull(cursor.getCurrentFieldName());
70 | }
71 |
72 | @Test
73 | void getCurrentDescendantIndex() {
74 | assertEquals(0, cursor.getCurrentDescendantIndex());
75 | }
76 |
77 | @Test
78 | void gotoFirstChild() {
79 | assertTrue(cursor.gotoFirstChild());
80 | assertEquals(1, cursor.getCurrentDepth());
81 | }
82 |
83 | @Test
84 | void gotoLastChild() {
85 | assertTrue(cursor.gotoLastChild());
86 | assertEquals(1, cursor.getCurrentDescendantIndex());
87 | }
88 |
89 | @Test
90 | void gotoParent() {
91 | assertFalse(cursor.gotoParent());
92 | }
93 |
94 | @Test
95 | void gotoNextSibling() {
96 | assertTrue(cursor.gotoFirstChild());
97 | assertFalse(cursor.gotoNextSibling());
98 | }
99 |
100 | @Test
101 | void gotoPreviousSibling() {
102 | assertTrue(cursor.gotoLastChild());
103 | assertFalse(cursor.gotoPreviousSibling());
104 | }
105 |
106 | @Test
107 | void gotoDescendant() {
108 | cursor.gotoDescendant(2);
109 | assertEquals(2, cursor.getCurrentDescendantIndex());
110 | assertEquals("class", cursor.getCurrentNode().getText());
111 | }
112 |
113 | @Test
114 | void gotoFirstChildForByte() {
115 | assertEquals(0, cursor.gotoFirstChildForByte(1).orElseThrow());
116 | assertEquals("class_declaration", cursor.getCurrentNode().getType());
117 | assertTrue(cursor.gotoFirstChildForByte(13).isEmpty());
118 | }
119 |
120 | @Test
121 | void gotoFirstChildForPoint() {
122 | assertTrue(cursor.gotoFirstChild());
123 | assertEquals(1, cursor.gotoFirstChildForPoint(new Point(0, 7)).orElseThrow());
124 | assertEquals("name", cursor.getCurrentFieldName());
125 | assertTrue(cursor.gotoFirstChildForPoint(new Point(1, 0)).isEmpty());
126 | }
127 |
128 | @Test
129 | @DisplayName("reset(node)")
130 | void resetNode() {
131 | var root = tree.getRootNode();
132 | assertTrue(cursor.gotoFirstChild());
133 | assertNotEquals(root, cursor.getCurrentNode());
134 | cursor.reset(root);
135 | assertEquals(root, cursor.getCurrentNode());
136 | }
137 |
138 | @Test
139 | @DisplayName("reset(cursor)")
140 | void resetCursor() {
141 | var copy = cursor.clone();
142 | var root = tree.getRootNode();
143 | assertTrue(cursor.gotoFirstChild());
144 | assertNotEquals(root, cursor.getCurrentNode());
145 | cursor.reset(copy);
146 | assertEquals(root, cursor.getCurrentNode());
147 | }
148 |
149 | @Test
150 | @DisplayName("clone()")
151 | void copy() {
152 | var copy = cursor.clone();
153 | assertNotSame(cursor, copy);
154 | assertTrue(copy.gotoFirstChild());
155 | assertNotEquals(cursor.getCurrentNode(), copy.getCurrentNode());
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/TreeTest.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava;
6 | import java.util.List;
7 | import java.util.concurrent.ExecutionException;
8 | import java.util.concurrent.Executors;
9 | import org.junit.jupiter.api.*;
10 |
11 | class TreeTest {
12 | private static final String source = "class Foo {}";
13 | private static Language language;
14 | private static Parser parser;
15 | private Tree tree;
16 |
17 | @BeforeAll
18 | static void beforeAll() {
19 | language = new Language(TreeSitterJava.language());
20 | parser = new Parser(language);
21 | }
22 |
23 | @AfterAll
24 | static void afterAll() {
25 | parser.close();
26 | }
27 |
28 | @BeforeEach
29 | void setUp() {
30 | tree = parser.parse(source).orElseThrow();
31 | }
32 |
33 | @AfterEach
34 | void tearDown() {
35 | tree.close();
36 | }
37 |
38 | @Test
39 | void getLanguage() {
40 | assertSame(language, tree.getLanguage());
41 | }
42 |
43 | @Test
44 | void getText() {
45 | assertEquals(source, tree.getText());
46 | }
47 |
48 | @Test
49 | void getRootNode() {
50 | assertEquals("program", tree.getRootNode().getType());
51 | }
52 |
53 | @Test
54 | void getRootNodeWithOffset() {
55 | var node = tree.getRootNodeWithOffset(6, new Point(0, 6));
56 | assertNotNull(node);
57 | assertEquals(source.substring(6), node.getText());
58 | }
59 |
60 | @Test
61 | void getIncludedRanges() {
62 | assertIterableEquals(List.of(Range.DEFAULT), tree.getIncludedRanges());
63 | }
64 |
65 | @Test
66 | void getChangedRanges() {
67 | tree.edit(new InputEdit(0, 0, 7, new Point(0, 0), new Point(0, 0), new Point(0, 7)));
68 | var newSource = "public %s".formatted(source);
69 | try (var newTree = parser.parse(newSource, tree).orElseThrow()) {
70 | var range = tree.getChangedRanges(newTree).getFirst();
71 | assertEquals(7, range.endByte());
72 | assertEquals(7, range.endPoint().column());
73 | }
74 | }
75 |
76 | @Test
77 | void edit() {
78 | tree.edit(new InputEdit(9, 9, 10, new Point(0, 10), new Point(0, 9), new Point(0, 10)));
79 | assertNull(tree.getText());
80 | }
81 |
82 | @Test
83 | void walk() {
84 | try (var cursor = tree.walk()) {
85 | assertEquals(tree.getRootNode(), cursor.getCurrentNode());
86 | }
87 | }
88 |
89 | @Test
90 | @DisplayName("clone()")
91 | void copy() {
92 | try (var exec = Executors.newSingleThreadExecutor()) {
93 | var result = exec.submit(() -> {
94 | try (var copy = tree.clone()) {
95 | assertNotSame(tree, copy);
96 | assertEquals(
97 | tree.getRootNode().toString(), copy.getRootNode().toString());
98 | }
99 | });
100 | result.get();
101 | } catch (InterruptedException | ExecutionException e) {
102 | fail(e);
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/test/java/io/github/treesitter/jtreesitter/languages/TreeSitterJava.java:
--------------------------------------------------------------------------------
1 | package io.github.treesitter.jtreesitter.languages;
2 |
3 | import java.lang.foreign.*;
4 |
5 | public final class TreeSitterJava {
6 | private static final ValueLayout VOID_PTR =
7 | ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_BYTE));
8 | private static final FunctionDescriptor FUNC_DESC = FunctionDescriptor.of(VOID_PTR);
9 | private static final Linker LINKER = Linker.nativeLinker();
10 | private static final TreeSitterJava INSTANCE = new TreeSitterJava();
11 |
12 | private final Arena arena = Arena.ofAuto();
13 | private final SymbolLookup symbols = findLibrary();
14 |
15 | /**
16 | * {@snippet lang=c :
17 | * const TSLanguage *tree_sitter_java()
18 | * }
19 | */
20 | public static MemorySegment language() {
21 | return INSTANCE.call("tree_sitter_java");
22 | }
23 |
24 | private SymbolLookup findLibrary() {
25 | try {
26 | var library = System.mapLibraryName("tree-sitter-java");
27 | return SymbolLookup.libraryLookup(library, arena);
28 | } catch (IllegalArgumentException e) {
29 | return SymbolLookup.loaderLookup();
30 | }
31 | }
32 |
33 | private static UnsatisfiedLinkError unresolved(String name) {
34 | return new UnsatisfiedLinkError("Unresolved symbol: %s".formatted(name));
35 | }
36 |
37 | @SuppressWarnings("SameParameterValue")
38 | private MemorySegment call(String name) throws UnsatisfiedLinkError {
39 | var address = symbols.find(name).orElseThrow(() -> unresolved(name));
40 | try {
41 | var function = LINKER.downcallHandle(address, FUNC_DESC);
42 | return (MemorySegment) function.invokeExact();
43 | } catch (Throwable e) {
44 | throw new RuntimeException("Call to %s failed".formatted(name), e);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------