Skip to content
This repository was archived by the owner on Oct 18, 2024. It is now read-only.

Commit 1bfacde

Browse files
committed
feat: add ability to wait for parse cancellation in TSParser
1 parent 0b69c66 commit 1bfacde

File tree

2 files changed

+94
-7
lines changed

2 files changed

+94
-7
lines changed

android-tree-sitter/src/main/java/com/itsaky/androidide/treesitter/TSParser.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.itsaky.androidide.treesitter.string.UTF16StringFactory;
2222
import com.itsaky.androidide.treesitter.util.TSObjectFactoryProvider;
2323
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.concurrent.locks.Condition;
2425
import java.util.concurrent.locks.ReentrantLock;
2526

2627
/**
@@ -30,6 +31,7 @@
3031
public class TSParser extends TSNativeObject {
3132

3233
protected final ReentrantLock parseLock = new ReentrantLock();
34+
protected final Condition parseCondition = parseLock.newCondition();
3335
protected final AtomicBoolean isParsing = new AtomicBoolean(false);
3436
protected final AtomicBoolean isCancellationRequested = new AtomicBoolean(false);
3537

@@ -157,9 +159,9 @@ public TSTree parseString(TSTree oldTree, String source) {
157159
* method.
158160
* <p>
159161
* Throws {@link ParseInProgressException} if the parser is currently parsing a syntax tree and
160-
* the cancellation was NOT requested using {@link #requestCancellation()}. This method blocks the
161-
* current thread if the previous parse was requested to be cancelled but the parse operation has
162-
* not been cancelled yet.
162+
* the cancellation was NOT requested using {@link #requestCancellationAsync()}. This method
163+
* blocks the current thread if the previous parse was requested to be cancelled but the parse
164+
* operation has not been cancelled yet.
163165
*
164166
* @param oldTree The previously parsed syntax tree.
165167
* @param source The source code to parse.
@@ -192,6 +194,7 @@ public TSTree parseString(TSTree oldTree, UTF16String source) {
192194
return createTree(tree);
193195
} finally {
194196
unsetParsingFlag();
197+
parseCondition.signalAll();
195198
parseLock.unlock();
196199
}
197200
}
@@ -245,17 +248,38 @@ protected boolean unsetParsingFlag() {
245248
* Request the parsing operation to be cancelled if the parser is in the process of parsing a
246249
* syntax tree.
247250
* <p>
248-
* The parse operation is NOT immediately cancelled.
251+
* This is an asynchronous operation and the previous parse call may NOT be cancelled immediately.
252+
* Use {@link #requestCancellationAndWait()} for a blocking cancellation request.
249253
*
250254
* @return <code>true</code> if the cancellation was requested successfully, <code>false</code>
251255
* otherwise.
252256
*/
253-
public boolean requestCancellation() {
257+
public boolean requestCancellationAsync() {
254258
final var requested = Native.requestCancellation();
255259
setCancellationRequested(requested);
256260
return requested;
257261
}
258262

263+
/**
264+
* If the parser is parsing syntax tree, sets the cancellation flag and blocks the current thread
265+
* until the parse operation returns. Does nothing if {@link #requestCancellationAsync()} returns
266+
* false.
267+
*/
268+
public void requestCancellationAndWait() {
269+
if (requestCancellationAsync()) {
270+
parseLock.lock();
271+
try {
272+
while (isParsing()) {
273+
parseCondition.await();
274+
}
275+
} catch (InterruptedException e) {
276+
throw new RuntimeException(e);
277+
} finally {
278+
parseLock.unlock();
279+
}
280+
}
281+
}
282+
259283
protected synchronized void setCancellationRequested(boolean isRequested) {
260284
this.isCancellationRequested.set(isRequested);
261285
}

android-tree-sitter/src/test/java/com/itsaky/androidide/treesitter/ParserTest.java

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ public void testParserCancellation() {
322322
final var parseFuture = executor.schedule(() -> {
323323

324324
// cancel the parsing after 200ms
325-
executor.schedule(() -> assertThat(parser.requestCancellation()).isTrue(), 200,
325+
executor.schedule(() -> assertThat(parser.requestCancellationAsync()).isTrue(), 200,
326326
TimeUnit.MILLISECONDS);
327327

328328
// parsing the View.java.txt file takes 300-600ms
@@ -336,6 +336,8 @@ public void testParserCancellation() {
336336
parseFuture.get();
337337
} catch (InterruptedException | ExecutionException e) {
338338
throw new RuntimeException(e);
339+
} finally {
340+
executor.shutdownNow();
339341
}
340342
}
341343
}
@@ -385,6 +387,8 @@ public void testParserParseCallShouldFailIfAnotherParseIsInProgress() {
385387
parseFuture2.get();
386388
} catch (Throwable e) {
387389
throw new RuntimeException(e);
390+
} finally {
391+
executor.shutdownNow();
388392
}
389393
}
390394
}
@@ -419,7 +423,7 @@ public void testParserParseCallShouldSucceedIfAnotherParseIsInProgressAndCancell
419423
assertThat(parser.isParsing()).isTrue();
420424

421425
// request the cancellation
422-
assertThat(parser.requestCancellation()).isTrue();
426+
assertThat(parser.requestCancellationAsync()).isTrue();
423427

424428
// the next parse call should wait for the previous parse call to return
425429
try (var tree = parser.parseString(fileContent)) {
@@ -436,6 +440,8 @@ public void testParserParseCallShouldSucceedIfAnotherParseIsInProgressAndCancell
436440
parseFuture2.get();
437441
} catch (Throwable e) {
438442
throw new RuntimeException(e);
443+
} finally {
444+
executor.shutdownNow();
439445
}
440446
}
441447
}
@@ -488,6 +494,63 @@ public void testParserParseCallShouldFailIfAnotherParseIsInProgressAndCancellati
488494
parseFuture2.get();
489495
} catch (Throwable e) {
490496
throw new RuntimeException(e);
497+
} finally {
498+
executor.shutdownNow();
499+
}
500+
}
501+
}
502+
503+
@Test
504+
public void testAwaitedCancellation() {
505+
try (final var parser = TSParser.create(); final var mainParseContent = UTF16StringFactory.newString()) {
506+
parser.setLanguage(TSLanguageJava.getInstance());
507+
508+
// Read the content before starting the threads
509+
final var fileContent = readResource("View.java.txt");
510+
mainParseContent.append(fileContent);
511+
mainParseContent.append(fileContent);
512+
mainParseContent.append(fileContent);
513+
514+
final var executor = Executors.newScheduledThreadPool(20);
515+
516+
// start the main parse operation immediately
517+
final var parseFuture1 = executor.schedule(() -> {
518+
try (final var tree = parser.parseString(mainParseContent)) {
519+
// This parse was cancelled and another parse was requested
520+
// so this should fail
521+
assertThat(tree).isNull();
522+
}
523+
}, 0, TimeUnit.MICROSECONDS);
524+
525+
// delay the second parse so that the parser is in the 'parsing' state when this is executed
526+
final var secondParseDelayMs = 100;
527+
final var parseFuture2 = executor.schedule(() -> {
528+
529+
// the parser should be in the 'parsing' state by now
530+
assertThat(parser.isParsing()).isTrue();
531+
532+
// request parse cancellation and wait till the parse returns
533+
final var start = System.currentTimeMillis();
534+
parser.requestCancellationAndWait();
535+
System.err.println("cancelAndWait() waited for " + (System.currentTimeMillis() - start) + "ms");
536+
537+
// request another parse
538+
try (var tree = parser.parseString(fileContent)) {
539+
// A parse was already in progress when this parse was requested
540+
// however, we cancelled that parse and requested this one
541+
// so this parseString call should succeed
542+
assertThat(tree).isNotNull();
543+
assertThat(tree.canAccess()).isTrue();
544+
}
545+
}, secondParseDelayMs, TimeUnit.MILLISECONDS);
546+
547+
try {
548+
parseFuture1.get();
549+
parseFuture2.get();
550+
} catch (Throwable e) {
551+
throw new RuntimeException(e);
552+
} finally {
553+
executor.shutdownNow();
491554
}
492555
}
493556
}

0 commit comments

Comments
 (0)