From b203aef71e9d81a40e08c15dcb085d5f63d17ae7 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Tue, 17 Sep 2024 22:07:01 +0200 Subject: [PATCH 01/11] Setup API tests --- build.gradle.kts | 3 + sample/src/main/kotlin/Main.kt | 1 + .../dev/snipme/highlights/Highlights.kt | 56 +- .../highlights/internal/HighlightsTest.kt | 1442 +++++++++++++++++ 4 files changed, 1490 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 875d93b..dbd59d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,12 +51,15 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") } } val commonTest by getting { dependencies { + // Coroutines test + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") implementation(kotlin("test")) } } diff --git a/sample/src/main/kotlin/Main.kt b/sample/src/main/kotlin/Main.kt index c1a7a52..c6310b5 100644 --- a/sample/src/main/kotlin/Main.kt +++ b/sample/src/main/kotlin/Main.kt @@ -3,6 +3,7 @@ import dev.snipme.highlights.model.BoldHighlight import dev.snipme.highlights.model.PhraseLocation import dev.snipme.highlights.model.SyntaxLanguage import dev.snipme.highlights.model.SyntaxThemes +import kotlin.concurrent.timer val sampleClass = """ @Serializable diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 018c0b0..2301745 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -10,6 +10,18 @@ import dev.snipme.highlights.model.PhraseLocation import dev.snipme.highlights.model.SyntaxLanguage import dev.snipme.highlights.model.SyntaxTheme import dev.snipme.highlights.model.SyntaxThemes +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface HighlightsResultListener { + fun onStart() + fun onCompleted(highlights: List) + fun onError(exception: Throwable) +} class Highlights private constructor( private var code: String, @@ -20,6 +32,9 @@ class Highlights private constructor( var snapshot: CodeSnapshot? = null private set + var analysisJob: Job? = null + private set + companion object { fun default() = fromBuilder(Builder()) @@ -60,22 +75,26 @@ class Highlights private constructor( } fun getHighlights(): List { - val highlights = mutableListOf() val structure = getCodeStructure() - with(structure) { - marks.forEach { highlights.add(ColorHighlight(it, theme.mark)) } - punctuations.forEach { highlights.add(ColorHighlight(it, theme.punctuation)) } - keywords.forEach { highlights.add(ColorHighlight(it, theme.keyword)) } - strings.forEach { highlights.add(ColorHighlight(it, theme.string)) } - literals.forEach { highlights.add(ColorHighlight(it, theme.literal)) } - annotations.forEach { highlights.add(ColorHighlight(it, theme.metadata)) } - comments.forEach { highlights.add(ColorHighlight(it, theme.comment)) } - multilineComments.forEach { highlights.add(ColorHighlight(it, theme.multilineComment)) } + return constructHighlights(structure) + } + + fun getHighlightsAsync(listener: HighlightsResultListener) { + analysisJob?.cancel() + + val errorHandler = CoroutineExceptionHandler { _, exception -> + listener.onError(exception) } - emphasisLocations.forEach { highlights.add(BoldHighlight(it)) } + analysisJob = CoroutineScope(Dispatchers.Default + errorHandler).launch { + val structure = getCodeStructure() + val highlights = constructHighlights(structure) + withContext(Dispatchers.Main) { + listener.onCompleted(highlights) + } + } - return highlights + listener.onStart() } fun getBuilder() = Builder(code, language, theme, emphasisLocations) @@ -87,4 +106,17 @@ class Highlights private constructor( fun getTheme() = theme fun getEmphasis() = emphasisLocations + + private fun constructHighlights(structure: CodeStructure): List = + mutableListOf().apply { + structure.marks.forEach { add(ColorHighlight(it, theme.mark)) } + structure.punctuations.forEach { add(ColorHighlight(it, theme.punctuation)) } + structure.keywords.forEach { add(ColorHighlight(it, theme.keyword)) } + structure.strings.forEach { add(ColorHighlight(it, theme.string)) } + structure.literals.forEach { add(ColorHighlight(it, theme.literal)) } + structure.annotations.forEach { add(ColorHighlight(it, theme.metadata)) } + structure.comments.forEach { add(ColorHighlight(it, theme.comment)) } + structure.multilineComments.forEach { add(ColorHighlight(it, theme.multilineComment)) } + emphasisLocations.forEach { add(BoldHighlight(it)) } + } } diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index 67a6b55..5d4a73a 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -1,32 +1,84 @@ package dev.snipme.highlights.internal import dev.snipme.highlights.Highlights +import dev.snipme.highlights.HighlightsResultListener +import dev.snipme.highlights.model.CodeHighlight +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertTrue +import kotlin.time.Duration +import kotlin.time.MonotonicTimeSource.markNow +import kotlin.time.TimeMark +import kotlin.time.TimeSource +import kotlin.time.TimeSource.Monotonic.markNow +import kotlin.time.measureTime +@OptIn(ExperimentalCoroutinesApi::class) class HighlightsTest { + private val testDispatcher = StandardTestDispatcher() + + @BeforeTest + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + @Test + fun `returns list of code highlights`() { + val default = Highlights.default().apply { + setCode(longTestCode()) + } fun `returns different keywords for default builder`() { val instance = Highlights.default() instance.setCode("class ") + val highlights = default.getHighlights() + assertTrue { highlights.isNotEmpty() } val highlights = instance.getHighlights() assertTrue(highlights.isNotEmpty()) } @Test + fun `returns list of code highlights asynchronously`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } fun `returns different keywords for default manual builder`() { val instance = Highlights.Builder().build() instance.setCode("class ") + val result = suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default) { + continuation.resume(it) {} + } + } val highlights = instance.getHighlights() + assertTrue { result.isNotEmpty() } assertTrue(highlights.isNotEmpty()) } @Test + fun `cancels first analysis when second is invoked`() { + val default = Highlights.default().apply { + setCode(longTestCode()) + } fun `returns highlights after code change`() { val instance = Highlights.default() @@ -36,29 +88,1419 @@ class HighlightsTest { instance.setCode("class ") + var job1: Job? + invokeHighlightsRequest( + default, + onStart = { + job1 = default.analysisJob + invokeHighlightsRequest(default, onStart = { + assertTrue { job1?.isActive == false } + assertTrue { default.analysisJob?.isActive == true } + }) + }, + ) val newHighlights = instance.getHighlights() assertTrue(newHighlights.isNotEmpty()) } @Test + fun `returns asynchronous results one by one`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + var result1: List + val time1 = measureTime { + result1 = suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default) { + continuation.resume(it) {} + } + } + } + assertTrue { result1.isNotEmpty() } + + default.setCode(longTestCode().replace("static", "statac")) + + var result2: List + val time2 = measureTime { + result2 = suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default) { + continuation.resume(it) {} + } + } + } + println("Time2: ${time2.inWholeMilliseconds} ms") + assertTrue { result2.isNotEmpty() } + + assertTrue { time2.inWholeMilliseconds < time1.inWholeMilliseconds } + } + + @Test + fun `returns immediately result from second invocation`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + // Set timestamp mark + val now = markNow() + + var result: List? = null + var time2: Duration = Duration.ZERO + val time1 = measureTime { + suspendCancellableCoroutine { + invokeHighlightsRequest( + default, + onStart = { + launch { + suspendCancellableCoroutine { continuation -> + time2 = measureTime { + invokeHighlightsRequest(default) { result -> assertTrue { result.isNotEmpty() } } + } + } + } + }) + } + } + + + assertTrue { time2.inWholeMilliseconds > time1.inWholeMilliseconds } + } + + private fun invokeHighlightsRequest( + highlights: Highlights, + onStart: () -> Unit = {}, + onError: (Throwable) -> Unit = {}, + onCompleted: (List) -> Unit = {}, + ) { + highlights.getHighlightsAsync(object : HighlightsResultListener { + override fun onStart() = onStart() + override fun onCompleted(highlights: List) = onCompleted(highlights) + override fun onError(exception: Throwable) = onError(exception) + }) + } + + private fun longTestCode() = """ + // + // Source code recreated from a .class file by IntelliJ IDEA + // (powered by FernFlower decompiler) + // + + package java.util; + + import java.io.IOException; + import java.io.InvalidObjectException; + import java.io.ObjectInputStream; + import java.io.ObjectOutputStream; + import java.io.Serializable; + import java.util.function.Consumer; + import java.util.function.Predicate; + import java.util.function.UnaryOperator; + import jdk.internal.access.SharedSecrets; + import jdk.internal.util.ArraysSupport; + + public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable { + private static final long serialVersionUID = 8683452581122892189L; + private static final int DEFAULT_CAPACITY = 10; + private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; + private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; + transient Object[] elementData; + private int size; + + public ArrayList(int var1) { + if (var1 > 0) { + this.elementData = new Object[var1]; + } else { + if (var1 != 0) { + throw new IllegalArgumentException("Illegal Capacity: " + var1); + } + + this.elementData = EMPTY_ELEMENTDATA; + } + + } + + public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; + } + + public ArrayList(Collection var1) { + Object[] var2 = var1.toArray(); + if ((this.size = var2.length) != 0) { + if (var1.getClass() == ArrayList.class) { + this.elementData = var2; + } else { + this.elementData = Arrays.copyOf(var2, this.size, Object[].class); + } + } else { + this.elementData = EMPTY_ELEMENTDATA; + } + + } + + public void trimToSize() { + ++this.modCount; + if (this.size < this.elementData.length) { + this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size); + } + + } + + public void ensureCapacity(int var1) { + if (var1 > this.elementData.length && (this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA || var1 > 10)) { + ++this.modCount; + this.grow(var1); + } + + } + + private Object[] grow(int var1) { + int var2 = this.elementData.length; + if (var2 <= 0 && this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + return this.elementData = new Object[Math.max(10, var1)]; + } else { + int var3 = ArraysSupport.newLength(var2, var1 - var2, var2 >> 1); + return this.elementData = Arrays.copyOf(this.elementData, var3); + } + } + + private Object[] grow() { + return this.grow(this.size + 1); + } + + public int size() { + return this.size; + } + + public boolean isEmpty() { + return this.size == 0; + } + + public boolean contains(Object var1) { + return this.indexOf(var1) >= 0; + } + + public int indexOf(Object var1) { + return this.indexOfRange(var1, 0, this.size); + } + + int indexOfRange(Object var1, int var2, int var3) { + Object[] var4 = this.elementData; + int var5; + if (var1 == null) { + for(var5 = var2; var5 < var3; ++var5) { + if (var4[var5] == null) { + return var5; + } + } + } else { + for(var5 = var2; var5 < var3; ++var5) { + if (var1.equals(var4[var5])) { + return var5; + } + } + } + + return -1; + } + + public int lastIndexOf(Object var1) { + return this.lastIndexOfRange(var1, 0, this.size); + } + + int lastIndexOfRange(Object var1, int var2, int var3) { + Object[] var4 = this.elementData; + int var5; + if (var1 == null) { + for(var5 = var3 - 1; var5 >= var2; --var5) { + if (var4[var5] == null) { + return var5; + } + } + } else { + for(var5 = var3 - 1; var5 >= var2; --var5) { + if (var1.equals(var4[var5])) { + return var5; + } + } + } + + return -1; + } + + public Object clone() { + try { + ArrayList var1 = (ArrayList)super.clone(); + var1.elementData = Arrays.copyOf(this.elementData, this.size); + var1.modCount = 0; + return var1; + } catch (CloneNotSupportedException var2) { + throw new InternalError(var2); + } + } + + public Object[] toArray() { + return Arrays.copyOf(this.elementData, this.size); + } + + public T[] toArray(T[] var1) { + if (var1.length < this.size) { + return Arrays.copyOf(this.elementData, this.size, var1.getClass()); + } else { + System.arraycopy(this.elementData, 0, var1, 0, this.size); + if (var1.length > this.size) { + var1[this.size] = null; + } + + return var1; + } + } + + E elementData(int var1) { + return this.elementData[var1]; + } + + static E elementAt(Object[] var0, int var1) { + return var0[var1]; + } + + public E get(int var1) { + Objects.checkIndex(var1, this.size); + return this.elementData(var1); + } + + public E set(int var1, E var2) { + Objects.checkIndex(var1, this.size); + Object var3 = this.elementData(var1); + this.elementData[var1] = var2; + return var3; + } + + private void add(E var1, Object[] var2, int var3) { + if (var3 == var2.length) { + var2 = this.grow(); + } + + var2[var3] = var1; + this.size = var3 + 1; + } + + public boolean add(E var1) { + ++this.modCount; + this.add(var1, this.elementData, this.size); + return true; + } + + public void add(int var1, E var2) { + this.rangeCheckForAdd(var1); + ++this.modCount; + int var3; + Object[] var4; + if ((var3 = this.size) == (var4 = this.elementData).length) { + var4 = this.grow(); + } + + System.arraycopy(var4, var1, var4, var1 + 1, var3 - var1); + var4[var1] = var2; + this.size = var3 + 1; + } + + public E remove(int var1) { + Objects.checkIndex(var1, this.size); + Object[] var2 = this.elementData; + Object var3 = var2[var1]; + this.fastRemove(var2, var1); + return var3; + } + + public boolean equals(Object var1) { + if (var1 == this) { + return true; + } else if (!(var1 instanceof List)) { + return false; + } else { + int var2 = this.modCount; + boolean var3 = var1.getClass() == ArrayList.class ? this.equalsArrayList((ArrayList)var1) : this.equalsRange((List)var1, 0, this.size); + this.checkForComodification(var2); + return var3; + } + } + + boolean equalsRange(List var1, int var2, int var3) { + Object[] var4 = this.elementData; + if (var3 > var4.length) { + throw new ConcurrentModificationException(); + } else { + Iterator var5; + for(var5 = var1.iterator(); var2 < var3; ++var2) { + if (!var5.hasNext() || !Objects.equals(var4[var2], var5.next())) { + return false; + } + } + + return !var5.hasNext(); + } + } + + private boolean equalsArrayList(ArrayList var1) { + int var2 = var1.modCount; + int var3 = this.size; + boolean var4; + if (var4 = var3 == var1.size) { + Object[] var5 = var1.elementData; + Object[] var6 = this.elementData; + if (var3 > var6.length || var3 > var5.length) { + throw new ConcurrentModificationException(); + } + + for(int var7 = 0; var7 < var3; ++var7) { + if (!Objects.equals(var6[var7], var5[var7])) { + var4 = false; + break; + } + } + } + + var1.checkForComodification(var2); + return var4; + } + + private void checkForComodification(int var1) { + if (this.modCount != var1) { + throw new ConcurrentModificationException(); + } + } + + public int hashCode() { + int var1 = this.modCount; + int var2 = this.hashCodeRange(0, this.size); + this.checkForComodification(var1); + return var2; + } + + int hashCodeRange(int var1, int var2) { + Object[] var3 = this.elementData; + if (var2 > var3.length) { + throw new ConcurrentModificationException(); + } else { + int var4 = 1; + + for(int var5 = var1; var5 < var2; ++var5) { + Object var6 = var3[var5]; + var4 = 31 * var4 + (var6 == null ? 0 : var6.hashCode()); + } + + return var4; + } + } + + public boolean remove(Object var1) { + Object[] var2 = this.elementData; + int var3 = this.size; + int var4 = 0; + if (var1 == null) { + while(true) { + if (var4 >= var3) { + return false; + } + + if (var2[var4] == null) { + break; + } + + ++var4; + } + } else { + while(true) { + if (var4 >= var3) { + return false; + } + + if (var1.equals(var2[var4])) { + break; + } + + ++var4; + } + } + + this.fastRemove(var2, var4); + return true; + } + + private void fastRemove(Object[] var1, int var2) { + ++this.modCount; + int var3; + if ((var3 = this.size - 1) > var2) { + System.arraycopy(var1, var2 + 1, var1, var2, var3 - var2); + } + + var1[this.size = var3] = null; + } + + public void clear() { + ++this.modCount; + Object[] var1 = this.elementData; + int var2 = this.size; + + for(int var3 = this.size = 0; var3 < var2; ++var3) { + var1[var3] = null; + } + + } + + public boolean addAll(Collection var1) { + Object[] var2 = var1.toArray(); + ++this.modCount; + int var3 = var2.length; + if (var3 == 0) { + return false; + } else { + Object[] var4; + int var5; + if (var3 > (var4 = this.elementData).length - (var5 = this.size)) { + var4 = this.grow(var5 + var3); + } + + System.arraycopy(var2, 0, var4, var5, var3); + this.size = var5 + var3; + return true; + } + } + + public boolean addAll(int var1, Collection var2) { + this.rangeCheckForAdd(var1); + Object[] var3 = var2.toArray(); + ++this.modCount; + int var4 = var3.length; + if (var4 == 0) { + return false; + } else { + Object[] var5; + int var6; + if (var4 > (var5 = this.elementData).length - (var6 = this.size)) { + var5 = this.grow(var6 + var4); + } + + int var7 = var6 - var1; + if (var7 > 0) { + System.arraycopy(var5, var1, var5, var1 + var4, var7); + } + + System.arraycopy(var3, 0, var5, var1, var4); + this.size = var6 + var4; + return true; + } + } + + protected void removeRange(int var1, int var2) { + if (var1 > var2) { + throw new IndexOutOfBoundsException(outOfBoundsMsg(var1, var2)); + } else { + ++this.modCount; + this.shiftTailOverGap(this.elementData, var1, var2); + } + } + + private void shiftTailOverGap(Object[] var1, int var2, int var3) { + System.arraycopy(var1, var3, var1, var2, this.size - var3); + int var4 = this.size; + + for(int var5 = this.size -= var3 - var2; var5 < var4; ++var5) { + var1[var5] = null; + } + + } + + private void rangeCheckForAdd(int var1) { + if (var1 > this.size || var1 < 0) { + throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1)); + } + } + + private String outOfBoundsMsg(int var1) { + return "Index: " + var1 + ", Size: " + this.size; + } + + private static String outOfBoundsMsg(int var0, int var1) { + return "From Index: " + var0 + " > To Index: " + var1; + } + + public boolean removeAll(Collection var1) { + return this.batchRemove(var1, false, 0, this.size); + } + + public boolean retainAll(Collection var1) { + return this.batchRemove(var1, true, 0, this.size); + } + + boolean batchRemove(Collection var1, boolean var2, int var3, int var4) { + Objects.requireNonNull(var1); + Object[] var5 = this.elementData; + + for(int var6 = var3; var6 != var4; ++var6) { + if (var1.contains(var5[var6]) != var2) { + int var7 = var6++; + + try { + for(; var6 < var4; ++var6) { + Object var8; + if (var1.contains(var8 = var5[var6]) == var2) { + var5[var7++] = var8; + } + } + } catch (Throwable var12) { + System.arraycopy(var5, var6, var5, var7, var4 - var6); + var7 += var4 - var6; + throw var12; + } finally { + this.modCount += var4 - var7; + this.shiftTailOverGap(var5, var7, var4); + } + + return true; + } + } + + return false; + } + + private void writeObject(ObjectOutputStream var1) throws IOException { + int var2 = this.modCount; + var1.defaultWriteObject(); + var1.writeInt(this.size); + + for(int var3 = 0; var3 < this.size; ++var3) { + var1.writeObject(this.elementData[var3]); + } + + if (this.modCount != var2) { + throw new ConcurrentModificationException(); + } + } + + private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { + var1.defaultReadObject(); + var1.readInt(); + if (this.size > 0) { + SharedSecrets.getJavaObjectInputStreamAccess().checkArray(var1, Object[].class, this.size); + Object[] var2 = new Object[this.size]; + + for(int var3 = 0; var3 < this.size; ++var3) { + var2[var3] = var1.readObject(); + } + + this.elementData = var2; + } else { + if (this.size != 0) { + throw new InvalidObjectException("Invalid size: " + this.size); + } + + this.elementData = EMPTY_ELEMENTDATA; + } + + } + + public ListIterator listIterator(int var1) { + this.rangeCheckForAdd(var1); + return new ListItr(var1); + } + + public ListIterator listIterator() { + return new ListItr(0); + } + + public Iterator iterator() { + return new Itr(); + } + + public List subList(int var1, int var2) { + subListRangeCheck(var1, var2, this.size); + return new SubList(this, var1, var2); + } + + public void forEach(Consumer var1) { + Objects.requireNonNull(var1); + int var2 = this.modCount; + Object[] var3 = this.elementData; + int var4 = this.size; + + for(int var5 = 0; this.modCount == var2 && var5 < var4; ++var5) { + var1.accept(elementAt(var3, var5)); + } + + if (this.modCount != var2) { + throw new ConcurrentModificationException(); + } + } + + public Spliterator spliterator() { + return new ArrayListSpliterator(0, -1, 0); + } + + private static long[] nBits(int var0) { + return new long[(var0 - 1 >> 6) + 1]; + } + + private static void setBit(long[] var0, int var1) { + var0[var1 >> 6] |= 1L << var1; + } + + private static boolean isClear(long[] var0, int var1) { + return (var0[var1 >> 6] & 1L << var1) == 0L; + } + + public boolean removeIf(Predicate var1) { + return this.removeIf(var1, 0, this.size); + } + + boolean removeIf(Predicate var1, int var2, int var3) { + Objects.requireNonNull(var1); + int var4 = this.modCount; + + Object[] var5; + for(var5 = this.elementData; var2 < var3 && !var1.test(elementAt(var5, var2)); ++var2) { + } + + if (var2 < var3) { + int var6 = var2; + long[] var7 = nBits(var3 - var2); + var7[0] = 1L; + ++var2; + + for(; var2 < var3; ++var2) { + if (var1.test(elementAt(var5, var2))) { + setBit(var7, var2 - var6); + } + } + + if (this.modCount != var4) { + throw new ConcurrentModificationException(); + } else { + ++this.modCount; + int var8 = var6; + + for(var2 = var6; var2 < var3; ++var2) { + if (isClear(var7, var2 - var6)) { + var5[var8++] = var5[var2]; + } + } + + this.shiftTailOverGap(var5, var8, var3); + return true; + } + } else if (this.modCount != var4) { + throw new ConcurrentModificationException(); + } else { + return false; + } + } + + public void replaceAll(UnaryOperator var1) { + this.replaceAllRange(var1, 0, this.size); + ++this.modCount; + } + + private void replaceAllRange(UnaryOperator var1, int var2, int var3) { + Objects.requireNonNull(var1); + int var4 = this.modCount; + + for(Object[] var5 = this.elementData; this.modCount == var4 && var2 < var3; ++var2) { + var5[var2] = var1.apply(elementAt(var5, var2)); + } + + if (this.modCount != var4) { + throw new ConcurrentModificationException(); + } + } + + public void sort(Comparator var1) { + int var2 = this.modCount; + Arrays.sort(this.elementData, 0, this.size, var1); + if (this.modCount != var2) { + throw new ConcurrentModificationException(); + } else { + ++this.modCount; + } + } + + void checkInvariants() { + } + + private class ListItr extends ArrayList.Itr implements ListIterator { + ListItr(int var2) { + super(); + this.cursor = var2; + } + + public boolean hasPrevious() { + return this.cursor != 0; + } + + public int nextIndex() { + return this.cursor; + } + + public int previousIndex() { + return this.cursor - 1; + } + + public E previous() { + this.checkForComodification(); + int var1 = this.cursor - 1; + if (var1 < 0) { + throw new NoSuchElementException(); + } else { + Object[] var2 = ArrayList.this.elementData; + if (var1 >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1; + return var2[this.lastRet = var1]; + } + } + } + + public void set(E var1) { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + ArrayList.this.set(this.lastRet, var1); + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + } + + public void add(E var1) { + this.checkForComodification(); + + try { + int var2 = this.cursor; + ArrayList.this.add(var2, var1); + this.cursor = var2 + 1; + this.lastRet = -1; + this.expectedModCount = ArrayList.this.modCount; + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + } + + private class Itr implements Iterator { + int cursor; + int lastRet = -1; + int expectedModCount; + + Itr() { + this.expectedModCount = ArrayList.this.modCount; + } + + public boolean hasNext() { + return this.cursor != ArrayList.this.size; + } + + public E next() { + this.checkForComodification(); + int var1 = this.cursor; + if (var1 >= ArrayList.this.size) { + throw new NoSuchElementException(); + } else { + Object[] var2 = ArrayList.this.elementData; + if (var1 >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1 + 1; + return var2[this.lastRet = var1]; + } + } + } + + public void remove() { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + ArrayList.this.remove(this.lastRet); + this.cursor = this.lastRet; + this.lastRet = -1; + this.expectedModCount = ArrayList.this.modCount; + } catch (IndexOutOfBoundsException var2) { + throw new ConcurrentModificationException(); + } + } + } + + public void forEachRemaining(Consumer var1) { + Objects.requireNonNull(var1); + int var2 = ArrayList.this.size; + int var3 = this.cursor; + if (var3 < var2) { + Object[] var4 = ArrayList.this.elementData; + if (var3 >= var4.length) { + throw new ConcurrentModificationException(); + } + + while(var3 < var2 && ArrayList.this.modCount == this.expectedModCount) { + var1.accept(ArrayList.elementAt(var4, var3)); + ++var3; + } + + this.cursor = var3; + this.lastRet = var3 - 1; + this.checkForComodification(); + } + + } + + final void checkForComodification() { + if (ArrayList.this.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + private static class SubList extends AbstractList implements RandomAccess { + private final ArrayList root; + private final SubList parent; + private final int offset; + private int size; + + public SubList(ArrayList var1, int var2, int var3) { + this.root = var1; + this.parent = null; + this.offset = var2; + this.size = var3 - var2; + this.modCount = var1.modCount; + } + + private SubList(SubList var1, int var2, int var3) { + this.root = var1.root; + this.parent = var1; + this.offset = var1.offset + var2; + this.size = var3 - var2; + this.modCount = var1.modCount; + } + + public E set(int var1, E var2) { + Objects.checkIndex(var1, this.size); + this.checkForComodification(); + Object var3 = this.root.elementData(this.offset + var1); + this.root.elementData[this.offset + var1] = var2; + return var3; + } + + public E get(int var1) { + Objects.checkIndex(var1, this.size); + this.checkForComodification(); + return this.root.elementData(this.offset + var1); + } + + public int size() { + this.checkForComodification(); + return this.size; + } + + public void add(int var1, E var2) { + this.rangeCheckForAdd(var1); + this.checkForComodification(); + this.root.add(this.offset + var1, var2); + this.updateSizeAndModCount(1); + } + + public E remove(int var1) { + Objects.checkIndex(var1, this.size); + this.checkForComodification(); + Object var2 = this.root.remove(this.offset + var1); + this.updateSizeAndModCount(-1); + return var2; + } + + protected void removeRange(int var1, int var2) { + this.checkForComodification(); + this.root.removeRange(this.offset + var1, this.offset + var2); + this.updateSizeAndModCount(var1 - var2); + } + + public boolean addAll(Collection var1) { + return this.addAll(this.size, var1); + } + + public boolean addAll(int var1, Collection var2) { + this.rangeCheckForAdd(var1); + int var3 = var2.size(); + if (var3 == 0) { + return false; + } else { + this.checkForComodification(); + this.root.addAll(this.offset + var1, var2); + this.updateSizeAndModCount(var3); + return true; + } + } + + public void replaceAll(UnaryOperator var1) { + this.root.replaceAllRange(var1, this.offset, this.offset + this.size); + } + + public boolean removeAll(Collection var1) { + return this.batchRemove(var1, false); + } + + public boolean retainAll(Collection var1) { + return this.batchRemove(var1, true); + } + + private boolean batchRemove(Collection var1, boolean var2) { + this.checkForComodification(); + int var3 = this.root.size; + boolean var4 = this.root.batchRemove(var1, var2, this.offset, this.offset + this.size); + if (var4) { + this.updateSizeAndModCount(this.root.size - var3); + } + + return var4; + } + + public boolean removeIf(Predicate var1) { + this.checkForComodification(); + int var2 = this.root.size; + boolean var3 = this.root.removeIf(var1, this.offset, this.offset + this.size); + if (var3) { + this.updateSizeAndModCount(this.root.size - var2); + } + + return var3; + } + + public Object[] toArray() { + this.checkForComodification(); + return Arrays.copyOfRange(this.root.elementData, this.offset, this.offset + this.size); + } + + public T[] toArray(T[] var1) { + this.checkForComodification(); + if (var1.length < this.size) { + return Arrays.copyOfRange(this.root.elementData, this.offset, this.offset + this.size, var1.getClass()); + } else { + System.arraycopy(this.root.elementData, this.offset, var1, 0, this.size); + if (var1.length > this.size) { + var1[this.size] = null; + } + + return var1; + } + } + + public boolean equals(Object var1) { + if (var1 == this) { + return true; + } else if (!(var1 instanceof List)) { + return false; + } else { + boolean var2 = this.root.equalsRange((List)var1, this.offset, this.offset + this.size); + this.checkForComodification(); + return var2; + } + } + + public int hashCode() { + int var1 = this.root.hashCodeRange(this.offset, this.offset + this.size); + this.checkForComodification(); + return var1; + } + + public int indexOf(Object var1) { + int var2 = this.root.indexOfRange(var1, this.offset, this.offset + this.size); + this.checkForComodification(); + return var2 >= 0 ? var2 - this.offset : -1; + } + + public int lastIndexOf(Object var1) { + int var2 = this.root.lastIndexOfRange(var1, this.offset, this.offset + this.size); + this.checkForComodification(); + return var2 >= 0 ? var2 - this.offset : -1; + } + + public boolean contains(Object var1) { + return this.indexOf(var1) >= 0; + } + + public Iterator iterator() { + return this.listIterator(); + } + + public ListIterator listIterator(final int var1) { + this.checkForComodification(); + this.rangeCheckForAdd(var1); + return new ListIterator() { + int cursor = var1; + int lastRet = -1; + int expectedModCount; + + { + this.expectedModCount = SubList.this.modCount; + } + + public boolean hasNext() { + return this.cursor != SubList.this.size; + } + + public E next() { + this.checkForComodification(); + int var1x = this.cursor; + if (var1x >= SubList.this.size) { + throw new NoSuchElementException(); + } else { + Object[] var2 = SubList.this.root.elementData; + if (SubList.this.offset + var1x >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1x + 1; + return var2[SubList.this.offset + (this.lastRet = var1x)]; + } + } + } + + public boolean hasPrevious() { + return this.cursor != 0; + } + + public E previous() { + this.checkForComodification(); + int var1x = this.cursor - 1; + if (var1x < 0) { + throw new NoSuchElementException(); + } else { + Object[] var2 = SubList.this.root.elementData; + if (SubList.this.offset + var1x >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1x; + return var2[SubList.this.offset + (this.lastRet = var1x)]; + } + } + } + + public void forEachRemaining(Consumer var1x) { + Objects.requireNonNull(var1x); + int var2 = SubList.this.size; + int var3 = this.cursor; + if (var3 < var2) { + Object[] var4 = SubList.this.root.elementData; + if (SubList.this.offset + var3 >= var4.length) { + throw new ConcurrentModificationException(); + } + + while(var3 < var2 && SubList.this.root.modCount == this.expectedModCount) { + var1x.accept(ArrayList.elementAt(var4, SubList.this.offset + var3)); + ++var3; + } + + this.cursor = var3; + this.lastRet = var3 - 1; + this.checkForComodification(); + } + + } + + public int nextIndex() { + return this.cursor; + } + + public int previousIndex() { + return this.cursor - 1; + } + + public void remove() { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + SubList.this.remove(this.lastRet); + this.cursor = this.lastRet; + this.lastRet = -1; + this.expectedModCount = SubList.this.modCount; + } catch (IndexOutOfBoundsException var2) { + throw new ConcurrentModificationException(); + } + } + } + + public void set(E var1x) { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + SubList.this.root.set(SubList.this.offset + this.lastRet, var1x); + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + } + + public void add(E var1x) { + this.checkForComodification(); + + try { + int var2 = this.cursor; + SubList.this.add(var2, var1x); + this.cursor = var2 + 1; + this.lastRet = -1; + this.expectedModCount = SubList.this.modCount; + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + + final void checkForComodification() { + if (SubList.this.root.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } + } + }; + } + + public List subList(int var1, int var2) { + subListRangeCheck(var1, var2, this.size); + return new SubList(this, var1, var2); + } + + private void rangeCheckForAdd(int var1) { + if (var1 < 0 || var1 > this.size) { + throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1)); + } + } + + private String outOfBoundsMsg(int var1) { + return "Index: " + var1 + ", Size: " + this.size; + } + + private void checkForComodification() { + if (this.root.modCount != this.modCount) { + throw new ConcurrentModificationException(); + } + } + + private void updateSizeAndModCount(int var1) { + SubList var2 = this; + + do { + var2.size += var1; + var2.modCount = this.root.modCount; + var2 = var2.parent; + } while(var2 != null); + + } fun `returns no highlights after cleared code`() { val instance = Highlights.default() val highlights = instance.getHighlights() + public Spliterator spliterator() { + this.checkForComodification(); + return new Spliterator() { + private int index; + private int fence; + private int expectedModCount; assertTrue(highlights.isEmpty()) + { + this.index = SubList.this.offset; + this.fence = -1; + } instance.setCode("class ") + private int getFence() { + int var1; + if ((var1 = this.fence) < 0) { + this.expectedModCount = SubList.this.modCount; + var1 = this.fence = SubList.this.offset + SubList.this.size; + } val newHighlights = instance.getHighlights() + return var1; + } assertTrue(newHighlights.isNotEmpty()) + public ArrayList.ArrayListSpliterator trySplit() { + int var1 = this.getFence(); + int var2 = this.index; + int var3 = var2 + var1 >>> 1; + ArrayListSpliterator var10000; + if (var2 >= var3) { + var10000 = null; + } else { + ArrayList var10002 = SubList.this.root; + Objects.requireNonNull(var10002); + var10000 = var10002.new ArrayListSpliterator(var2, this.index = var3, this.expectedModCount); + } instance.setCode("") + return var10000; + } val emptyHighlights = instance.getHighlights() + public boolean tryAdvance(Consumer var1) { + Objects.requireNonNull(var1); + int var2 = this.getFence(); + int var3 = this.index; + if (var3 < var2) { + this.index = var3 + 1; + Object var4 = SubList.this.root.elementData[var3]; + var1.accept(var4); + if (SubList.this.root.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } else { + return true; + } + } else { + return false; + } + } assertTrue(emptyHighlights.isEmpty()) } + public void forEachRemaining(Consumer var1) { + Objects.requireNonNull(var1); + ArrayList var5 = SubList.this.root; + Object[] var6; + if ((var6 = var5.elementData) != null) { + int var3; + int var4; + if ((var3 = this.fence) < 0) { + var4 = SubList.this.modCount; + var3 = SubList.this.offset + SubList.this.size; + } else { + var4 = this.expectedModCount; + } + + int var2; + if ((var2 = this.index) >= 0 && (this.index = var3) <= var6.length) { + while(var2 < var3) { + Object var7 = var6[var2]; + var1.accept(var7); + ++var2; + } + + if (var5.modCount == var4) { + return; + } + } + } + + throw new ConcurrentModificationException(); + } + + public long estimateSize() { + return (long)(this.getFence() - this.index); + } + + public int characteristics() { + return 16464; + } + }; + } + } + + final class ArrayListSpliterator implements Spliterator { + private int index; + private int fence; + private int expectedModCount; + + ArrayListSpliterator(int var2, int var3, int var4) { + this.index = var2; + this.fence = var3; + this.expectedModCount = var4; + } + + private int getFence() { + int var1; + if ((var1 = this.fence) < 0) { + this.expectedModCount = ArrayList.this.modCount; + var1 = this.fence = ArrayList.this.size; + } + + return var1; + } + + public ArrayList.ArrayListSpliterator trySplit() { + int var1 = this.getFence(); + int var2 = this.index; + int var3 = var2 + var1 >>> 1; + return var2 >= var3 ? null : ArrayList.this.new ArrayListSpliterator(var2, this.index = var3, this.expectedModCount); + } + + public boolean tryAdvance(Consumer var1) { + if (var1 == null) { + throw new NullPointerException(); + } else { + int var2 = this.getFence(); + int var3 = this.index; + if (var3 < var2) { + this.index = var3 + 1; + Object var4 = ArrayList.this.elementData[var3]; + var1.accept(var4); + if (ArrayList.this.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } else { + return true; + } + } else { + return false; + } + } + } + + public void forEachRemaining(Consumer var1) { + if (var1 == null) { + throw new NullPointerException(); + } else { + Object[] var5; + if ((var5 = ArrayList.this.elementData) != null) { + int var3; + int var4; + if ((var3 = this.fence) < 0) { + var4 = ArrayList.this.modCount; + var3 = ArrayList.this.size; + } else { + var4 = this.expectedModCount; + } + + int var2; + if ((var2 = this.index) >= 0 && (this.index = var3) <= var5.length) { + while(var2 < var3) { + Object var6 = var5[var2]; + var1.accept(var6); + ++var2; + } + + if (ArrayList.this.modCount == var4) { + return; + } + } + } + + throw new ConcurrentModificationException(); + } + } + + public long estimateSize() { + return (long)(this.getFence() - this.index); + } + + public int characteristics() { + return 16464; + } + } + } + """.trimIndent() } \ No newline at end of file From 929ecdd3ff0a2c9ff5cd8a7f75c830cbf57f0f5b Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Wed, 18 Sep 2024 17:29:07 +0200 Subject: [PATCH 02/11] Corrected cancelling --- .../dev/snipme/highlights/Highlights.kt | 44 +++++++++---- .../highlights/internal/HighlightsTest.kt | 63 ++++++++++++++----- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 2301745..85c7889 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -16,11 +16,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException interface HighlightsResultListener { fun onStart() - fun onCompleted(highlights: List) + fun onComplete(highlights: List) fun onError(exception: Throwable) + fun onCancel() } class Highlights private constructor( @@ -80,21 +82,39 @@ class Highlights private constructor( } fun getHighlightsAsync(listener: HighlightsResultListener) { - analysisJob?.cancel() + var thisJob: Job? = null - val errorHandler = CoroutineExceptionHandler { _, exception -> - listener.onError(exception) - } + try { + analysisJob?.cancel() - analysisJob = CoroutineScope(Dispatchers.Default + errorHandler).launch { - val structure = getCodeStructure() - val highlights = constructHighlights(structure) - withContext(Dispatchers.Main) { - listener.onCompleted(highlights) + val errorHandler = CoroutineExceptionHandler { _, exception -> + listener.onError(exception) + } + + analysisJob = CoroutineScope(Dispatchers.Default + errorHandler).launch { + println("---START---") + val structure = getCodeStructure() + val highlights = constructHighlights(structure) + println("---END---") + withContext(Dispatchers.Main) { + listener.onComplete(highlights) + } } - } - listener.onStart() + thisJob = analysisJob + thisJob?.invokeOnCompletion { + if (it is CancellationException) { + listener.onCancel() + } + } + listener.onStart() + } catch (exception: Exception) { + listener.onError(exception) + } finally { + if (thisJob?.isActive == false) { + listener.onCancel() + } + } } fun getBuilder() = Builder(code, language, theme, emphasisLocations) diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index 5d4a73a..e72fc90 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -3,23 +3,24 @@ package dev.snipme.highlights.internal import dev.snipme.highlights.Highlights import dev.snipme.highlights.HighlightsResultListener import dev.snipme.highlights.model.CodeHighlight +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import kotlin.coroutines.resumeWithException import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertTrue import kotlin.time.Duration -import kotlin.time.MonotonicTimeSource.markNow -import kotlin.time.TimeMark -import kotlin.time.TimeSource import kotlin.time.TimeSource.Monotonic.markNow import kotlin.time.measureTime @@ -142,41 +143,73 @@ class HighlightsTest { setCode(longTestCode()) } - // Set timestamp mark - val now = markNow() + val scope = this - var result: List? = null + val now = markNow() + var time1: Duration = Duration.ZERO var time2: Duration = Duration.ZERO - val time1 = measureTime { - suspendCancellableCoroutine { + + suspendCancellableCoroutine { continuation -> invokeHighlightsRequest( default, onStart = { - launch { - suspendCancellableCoroutine { continuation -> - time2 = measureTime { - invokeHighlightsRequest(default) { result -> assertTrue { result.isNotEmpty() } } + println("First start") + scope.launch { + suspendCancellableCoroutine> { continuation2 -> + invokeHighlightsRequest(default) { result -> + time2 = now.elapsedNow() + assertTrue { result.isNotEmpty() } + continuation2.resume(result) {} } } } - }) - } + }, + onCancel = { + time1 = now.elapsedNow() + println("First cancelled") + continuation.resume(Unit) {} + }, + ) } + println("Time1: ${time1.inWholeMilliseconds} ms") + println("Time2: ${time2.inWholeMilliseconds} ms") assertTrue { time2.inWholeMilliseconds > time1.inWholeMilliseconds } } + @Test + fun `returns cancellation result`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + var cancelled = false + suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default, + onStart = { default.analysisJob?.cancel() }, + onCancel = { + cancelled = true + continuation.resume(Unit) {} + } + ) + } + + assertTrue { cancelled } + } + private fun invokeHighlightsRequest( highlights: Highlights, onStart: () -> Unit = {}, + onCancel: () -> Unit = {}, onError: (Throwable) -> Unit = {}, onCompleted: (List) -> Unit = {}, ) { highlights.getHighlightsAsync(object : HighlightsResultListener { override fun onStart() = onStart() - override fun onCompleted(highlights: List) = onCompleted(highlights) + override fun onComplete(highlights: List) = onCompleted(highlights) override fun onError(exception: Throwable) = onError(exception) + override fun onCancel() = onCancel() }) } From b03e0879f8f31fce885df84b8af075958046c175 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Sun, 22 Sep 2024 10:18:26 +0200 Subject: [PATCH 03/11] Tested new async method --- .../dev/snipme/highlights/Highlights.kt | 39 ++-- .../snipme/highlights/internal/Extensions.kt | 10 + .../highlights/internal/HighlightsTest.kt | 193 ++++++++++++++---- 3 files changed, 180 insertions(+), 62 deletions(-) diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 85c7889..76d697f 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -2,6 +2,7 @@ package dev.snipme.highlights import dev.snipme.highlights.internal.CodeAnalyzer import dev.snipme.highlights.internal.CodeSnapshot +import dev.snipme.highlights.internal.onCancel import dev.snipme.highlights.model.BoldHighlight import dev.snipme.highlights.model.CodeHighlight import dev.snipme.highlights.model.CodeStructure @@ -16,7 +17,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlin.coroutines.cancellation.CancellationException interface HighlightsResultListener { fun onStart() @@ -82,38 +82,33 @@ class Highlights private constructor( } fun getHighlightsAsync(listener: HighlightsResultListener) { - var thisJob: Job? = null - try { - analysisJob?.cancel() - val errorHandler = CoroutineExceptionHandler { _, exception -> listener.onError(exception) } - analysisJob = CoroutineScope(Dispatchers.Default + errorHandler).launch { - println("---START---") - val structure = getCodeStructure() - val highlights = constructHighlights(structure) - println("---END---") - withContext(Dispatchers.Main) { - listener.onComplete(highlights) + fun runJob() { + CoroutineScope(Dispatchers.Default + errorHandler).launch { + val structure = getCodeStructure() + val highlights = constructHighlights(structure) + withContext(Dispatchers.Main) { + listener.onComplete(highlights) + } + }.also { + analysisJob = it + analysisJob!!.onCancel { listener.onCancel() } + listener.onStart() } } - thisJob = analysisJob - thisJob?.invokeOnCompletion { - if (it is CancellationException) { - listener.onCancel() - } + if (analysisJob == null || analysisJob?.isCompleted == true) { + runJob() + } else { + analysisJob!!.onCancel { runJob() } + analysisJob?.cancel() } - listener.onStart() } catch (exception: Exception) { listener.onError(exception) - } finally { - if (thisJob?.isActive == false) { - listener.onCancel() - } } } diff --git a/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt b/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt index 3bb9fa4..a03aa3c 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt @@ -4,6 +4,8 @@ import dev.snipme.highlights.model.CodeHighlight import dev.snipme.highlights.model.PhraseLocation import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.coroutines.Job +import kotlin.coroutines.cancellation.CancellationException fun List.toJson(): String { return Json.encodeToString>(this) @@ -88,4 +90,12 @@ fun Set.toRangeSet(): Set = operator fun IntRange.contains(range: IntRange): Boolean { return range.first >= this.first && range.last <= this.last +} + +fun Job.onCancel(block: () -> Unit) { + invokeOnCompletion { + if (it is CancellationException) { + block() + } + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index e72fc90..e80408d 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -3,19 +3,16 @@ package dev.snipme.highlights.internal import dev.snipme.highlights.Highlights import dev.snipme.highlights.HighlightsResultListener import dev.snipme.highlights.model.CodeHighlight -import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import kotlin.coroutines.resumeWithException import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -26,7 +23,6 @@ import kotlin.time.measureTime @OptIn(ExperimentalCoroutinesApi::class) class HighlightsTest { - private val testDispatcher = StandardTestDispatcher() @BeforeTest @@ -56,7 +52,7 @@ class HighlightsTest { } @Test - fun `returns list of code highlights asynchronously`() = runTest { + fun `returns null job before first invocation`() { val default = Highlights.default().apply { setCode(longTestCode()) } @@ -64,14 +60,60 @@ class HighlightsTest { val instance = Highlights.Builder().build() instance.setCode("class ") - val result = suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest(default) { - continuation.resume(it) {} - } + assertTrue { default.analysisJob == null } + } + + @Test + fun `returns active job after start`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) } val highlights = instance.getHighlights() - assertTrue { result.isNotEmpty() } + suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default, onStart = { + assertTrue { default.analysisJob?.isActive == true } + continuation.resume(Unit) {} + }) + } + } + + @Test + fun `returns inactive job after completion`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default, + onStart = { + default.analysisJob?.invokeOnCompletion { + assertTrue { default.analysisJob?.isActive == false } + } + } + ) { continuation.resume(Unit) {} } + } + } + + @Test + fun `returns error for exception during analysis`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + var error: Throwable? = null + suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest( + default, + onStart = { throw IllegalStateException() }, + onError = { + error = it + continuation.resume(Unit) {} + }, + ) + } + + assertTrue { error != null } assertTrue(highlights.isNotEmpty()) } @@ -105,6 +147,56 @@ class HighlightsTest { assertTrue(newHighlights.isNotEmpty()) } + @Test + fun `returns active job for each invocation`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + val defaultJob = Job(null) as Job + + var job1 = defaultJob + suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default, + onStart = { + job1 = default.analysisJob!! + assertTrue { job1.isActive } + }, + onCompleted = { + continuation.resume(it) {} + }) + } + + var job2 = defaultJob + suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default, + onStart = { + job2 = default.analysisJob!! + assertTrue { job2.isActive } + }, + onCompleted = { + continuation.resume(it) {} + }) + } + + assertTrue { job1 != job2 } + } + + @Test + fun `returns list of code highlights asynchronously`() = runTest { + val default = Highlights.default().apply { + setCode(longTestCode()) + } + + val result = suspendCancellableCoroutine { continuation -> + invokeHighlightsRequest(default) { + continuation.resume(it) {} + } + } + + assertTrue { result.isNotEmpty() } + } + @Test fun `returns asynchronous results one by one`() = runTest { val default = Highlights.default().apply { @@ -119,6 +211,7 @@ class HighlightsTest { } } } + println("Time1: ${time1.inWholeMilliseconds} ms") assertTrue { result1.isNotEmpty() } default.setCode(longTestCode().replace("static", "statac")) @@ -143,39 +236,28 @@ class HighlightsTest { setCode(longTestCode()) } - val scope = this - - val now = markNow() - var time1: Duration = Duration.ZERO - var time2: Duration = Duration.ZERO + var time1: Duration + var time2: Duration - suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest( - default, - onStart = { - println("First start") - scope.launch { - suspendCancellableCoroutine> { continuation2 -> - invokeHighlightsRequest(default) { result -> - time2 = now.elapsedNow() - assertTrue { result.isNotEmpty() } - continuation2.resume(result) {} - } - } - } - }, - onCancel = { - time1 = now.elapsedNow() - println("First cancelled") - continuation.resume(Unit) {} - }, - ) + launch { + suspendCancellableCoroutine { c -> + invokeAndMeasureTime(default) { + time1 = it + c.resume(Unit) {} + println("Time1: ${time1.inWholeMilliseconds} ms") + } + } } - println("Time1: ${time1.inWholeMilliseconds} ms") - println("Time2: ${time2.inWholeMilliseconds} ms") - - assertTrue { time2.inWholeMilliseconds > time1.inWholeMilliseconds } + launch { + suspendCancellableCoroutine { c -> + invokeAndMeasureTime(default) { + time2 = it + c.resume(Unit) {} + println("Time2: ${time2.inWholeMilliseconds} ms") + } + } + } } @Test @@ -198,6 +280,37 @@ class HighlightsTest { assertTrue { cancelled } } + private fun invokeAndMeasureTime( + highlights: Highlights, + onFinish: (Duration) -> Unit = {} + ) { + var result: Duration? = null + val now = markNow() + + fun updateFirstTime() { + if (result == null) { + result = now.elapsedNow() + onFinish(result!!) + } + } + + fun listen() { + highlights.analysisJob?.invokeOnCompletion { + if (it is CancellationException) { + updateFirstTime() + } + } + } + + invokeHighlightsRequest( + highlights, + onStart = { println("Start"); listen() }, + onCancel = { println("Cancel"); updateFirstTime() }, + onError = { println("Error: $it"); updateFirstTime() }, + onCompleted = { println("Completed"); updateFirstTime() }, + ) + } + private fun invokeHighlightsRequest( highlights: Highlights, onStart: () -> Unit = {}, From 78f6c00e3a63a0b2c57a1add825ecc1c6a2696c4 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Sun, 22 Sep 2024 10:55:01 +0200 Subject: [PATCH 04/11] Added async code to sample --- .DS_Store | Bin 6148 -> 8196 bytes build.gradle.kts | 1 - sample/src/main/kotlin/Main.kt | 47 ++++++++++++++++-- .../dev/snipme/highlights/Highlights.kt | 7 --- .../highlights/HighlightsResultListener.kt | 17 +++++++ 5 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt diff --git a/.DS_Store b/.DS_Store index 6d2eb02fb9b3741e22bde04e6edd4ec228262126..274b614a1f90e9f51d76772ce1be6d960ffb3c52 100644 GIT binary patch delta 133 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD6}zPH}hr%jz7$tj6iW9;NadY z$RWnES&io*^TY-gMxo6-ELqH)X(`3YN%{FXjFZ#Y7j9PIdB@Bp!3|XI3Nj3+<2&(JnZpbKzmgi( delta 112 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50$SAlmU^g?P;AS3yEau7m!V@_e yfwDlr!M(9ioMp4R$a7{c0dAmxD@fDE!tczJ`DHvoMldizj04%gusNP*4l@8ad=jky diff --git a/build.gradle.kts b/build.gradle.kts index dbd59d2..46ecd3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,7 +58,6 @@ kotlin { val commonTest by getting { dependencies { - // Coroutines test implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") implementation(kotlin("test")) } diff --git a/sample/src/main/kotlin/Main.kt b/sample/src/main/kotlin/Main.kt index c6310b5..a02d3ee 100644 --- a/sample/src/main/kotlin/Main.kt +++ b/sample/src/main/kotlin/Main.kt @@ -1,9 +1,10 @@ +import dev.snipme.highlights.DefaultHighlightsResultListener import dev.snipme.highlights.Highlights import dev.snipme.highlights.model.BoldHighlight +import dev.snipme.highlights.model.CodeHighlight import dev.snipme.highlights.model.PhraseLocation import dev.snipme.highlights.model.SyntaxLanguage import dev.snipme.highlights.model.SyntaxThemes -import kotlin.concurrent.timer val sampleClass = """ @Serializable @@ -31,7 +32,24 @@ val sampleClass = """ } } """.trimIndent() + fun main() { + val syncResult = runSync() + testAsync { asyncResult -> + + println() + + println("Sync result:") + println(syncResult) + println() + + println("Async result:") + println(asyncResult) + + } +} + +fun runSync(): List { println("### HIGHLIGHTS ###") println() @@ -64,11 +82,34 @@ fun main() { .build() println("The emphasis was put on the word:") - val emphasisLocation = newInstance - .getHighlights() + val result = newInstance.getHighlights() + val emphasisLocation = result .filterIsInstance() .first() .location println(sampleClass.substring(emphasisLocation.start, emphasisLocation.end)) + + return result +} + +fun testAsync(emitResult: (List) -> Unit) { + println("### ASYNC HIGHLIGHTS ###") + + val highlights = Highlights.Builder() + .code(sampleClass) + .theme(SyntaxThemes.monokai()) + .language(SyntaxLanguage.JAVA) + .build() + + highlights.getHighlightsAsync( + object : DefaultHighlightsResultListener() { + // onStart + // onError + // onCancel + override fun onComplete(highlights: List) { + emitResult(highlights) + } + } + ) } \ No newline at end of file diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 76d697f..2e0ff02 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -18,13 +18,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -interface HighlightsResultListener { - fun onStart() - fun onComplete(highlights: List) - fun onError(exception: Throwable) - fun onCancel() -} - class Highlights private constructor( private var code: String, private val language: SyntaxLanguage, diff --git a/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt b/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt new file mode 100644 index 0000000..03cd1a9 --- /dev/null +++ b/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt @@ -0,0 +1,17 @@ +package dev.snipme.highlights + +import dev.snipme.highlights.model.CodeHighlight + +interface HighlightsResultListener { + fun onStart() + fun onComplete(highlights: List) + fun onError(exception: Throwable) + fun onCancel() +} + +abstract class DefaultHighlightsResultListener : HighlightsResultListener { + override fun onStart() {} + override fun onComplete(highlights: List) {} + override fun onError(exception: Throwable) {} + override fun onCancel() {} +} \ No newline at end of file From 71239b2878d80c58b02f6bb309a1f69b42ef4867 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Sun, 22 Sep 2024 11:48:25 +0200 Subject: [PATCH 05/11] Created async example --- README.md | 16 ++++++ sample/build.gradle.kts | 2 + sample/src/main/kotlin/Main.kt | 54 ++++++++++--------- .../dev/snipme/highlights/Highlights.kt | 5 +- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 4a1186f..0953e3a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ implementation("dev.snipme:highlights:1.0.0") - Text bolding (emphasis) - Result caching and support for incremental changes - Written in pure Kotlin, so available for many platforms 📱 💻 🖥️ +- Sync or async mode ## Support ☕ Kotlin Multiplatform is a fresh environment and developing for it is neither fast nor easy 🥲 @@ -48,6 +49,21 @@ Highlights.default().apply { } ``` +There is also a possibility to handle result asynchronously + +```kotlin + highlights.getHighlightsAsync( + object : DefaultHighlightsResultListener() { + // onStart + // onError + // onCancel + override fun onComplete(highlights: List) { + emitResult(highlights) + } + } + ) +``` + You can also set language, theme and phrase emphasis. Language and theme has impact on the ColorHighlight and emphasis is represented by the BoldHighlight. diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 451cb50..eb44742 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -16,6 +16,8 @@ repositories { dependencies { implementation("dev.snipme:highlights:1.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") + testImplementation(kotlin("test")) } diff --git a/sample/src/main/kotlin/Main.kt b/sample/src/main/kotlin/Main.kt index a02d3ee..864075b 100644 --- a/sample/src/main/kotlin/Main.kt +++ b/sample/src/main/kotlin/Main.kt @@ -5,6 +5,9 @@ import dev.snipme.highlights.model.CodeHighlight import dev.snipme.highlights.model.PhraseLocation import dev.snipme.highlights.model.SyntaxLanguage import dev.snipme.highlights.model.SyntaxThemes +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine val sampleClass = """ @Serializable @@ -34,22 +37,29 @@ val sampleClass = """ """.trimIndent() fun main() { - val syncResult = runSync() - testAsync { asyncResult -> - - println() - - println("Sync result:") - println(syncResult) - println() - - println("Async result:") - println(asyncResult) - + runBlocking { + val highlights = Highlights.Builder() + .code(sampleClass) + .theme(SyntaxThemes.monokai()) + .language(SyntaxLanguage.JAVA) + .build() + + val syncResult = runSync(highlights) + println("Sync count with emphasis: ${syncResult.size}") + + launch { + suspendCancellableCoroutine { continuation -> + runAsync(highlights) { asyncResult -> + assert(syncResult == asyncResult) + println("Async count: ${asyncResult.size}") + continuation.resumeWith(Result.success(Unit)) + } + } + } } } -fun runSync(): List { +fun runSync(highlights: Highlights): List { println("### HIGHLIGHTS ###") println() @@ -65,12 +75,6 @@ fun runSync(): List { println(sampleClass) println() - val highlights = Highlights.Builder() - .code(sampleClass) - .theme(SyntaxThemes.monokai()) - .language(SyntaxLanguage.JAVA) - .build() - val structure = highlights.getCodeStructure() println("After analysis there has been found:") @@ -93,15 +97,13 @@ fun runSync(): List { return result } -fun testAsync(emitResult: (List) -> Unit) { +fun runAsync( + highlights: Highlights, + emitResult: (List) -> Unit, +) { + println() println("### ASYNC HIGHLIGHTS ###") - val highlights = Highlights.Builder() - .code(sampleClass) - .theme(SyntaxThemes.monokai()) - .language(SyntaxLanguage.JAVA) - .build() - highlights.getHighlightsAsync( object : DefaultHighlightsResultListener() { // onStart diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 2e0ff02..c3656bc 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class Highlights private constructor( private var code: String, @@ -84,9 +83,7 @@ class Highlights private constructor( CoroutineScope(Dispatchers.Default + errorHandler).launch { val structure = getCodeStructure() val highlights = constructHighlights(structure) - withContext(Dispatchers.Main) { - listener.onComplete(highlights) - } + listener.onComplete(highlights) }.also { analysisJob = it analysisJob!!.onCancel { listener.onCancel() } From 231daf893190fed7e6798c1e346388fc7016e3b4 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Sun, 22 Sep 2024 11:53:57 +0200 Subject: [PATCH 06/11] Extracted long java code --- .gitignore | 1 + .../snipme/highlights/internal/CodeSamples.kt | 1314 +++++++++++++++++ .../highlights/internal/HighlightsTest.kt | 24 +- 3 files changed, 1327 insertions(+), 12 deletions(-) create mode 100644 src/commonTest/kotlin/dev/snipme/highlights/internal/CodeSamples.kt diff --git a/.gitignore b/.gitignore index 5b6c852..0881554 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ gradle-app.setting .classpath # MacOS files *.DS_Store +/sample/*.DS_Store diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/CodeSamples.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/CodeSamples.kt new file mode 100644 index 0000000..fd89b17 --- /dev/null +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/CodeSamples.kt @@ -0,0 +1,1314 @@ +package dev.snipme.highlights.internal + +val longJavaCode = """ + // + // Source code recreated from a .class file by IntelliJ IDEA + // (powered by FernFlower decompiler) + // + + package java.util; + + import java.io.IOException; + import java.io.InvalidObjectException; + import java.io.ObjectInputStream; + import java.io.ObjectOutputStream; + import java.io.Serializable; + import java.util.function.Consumer; + import java.util.function.Predicate; + import java.util.function.UnaryOperator; + import jdk.internal.access.SharedSecrets; + import jdk.internal.util.ArraysSupport; + + public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable { + private static final long serialVersionUID = 8683452581122892189L; + private static final int DEFAULT_CAPACITY = 10; + private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; + private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; + transient Object[] elementData; + private int size; + + public ArrayList(int var1) { + if (var1 > 0) { + this.elementData = new Object[var1]; + } else { + if (var1 != 0) { + throw new IllegalArgumentException("Illegal Capacity: " + var1); + } + + this.elementData = EMPTY_ELEMENTDATA; + } + + } + + public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; + } + + public ArrayList(Collection var1) { + Object[] var2 = var1.toArray(); + if ((this.size = var2.length) != 0) { + if (var1.getClass() == ArrayList.class) { + this.elementData = var2; + } else { + this.elementData = Arrays.copyOf(var2, this.size, Object[].class); + } + } else { + this.elementData = EMPTY_ELEMENTDATA; + } + + } + + public void trimToSize() { + ++this.modCount; + if (this.size < this.elementData.length) { + this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size); + } + + } + + public void ensureCapacity(int var1) { + if (var1 > this.elementData.length && (this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA || var1 > 10)) { + ++this.modCount; + this.grow(var1); + } + + } + + private Object[] grow(int var1) { + int var2 = this.elementData.length; + if (var2 <= 0 && this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + return this.elementData = new Object[Math.max(10, var1)]; + } else { + int var3 = ArraysSupport.newLength(var2, var1 - var2, var2 >> 1); + return this.elementData = Arrays.copyOf(this.elementData, var3); + } + } + + private Object[] grow() { + return this.grow(this.size + 1); + } + + public int size() { + return this.size; + } + + public boolean isEmpty() { + return this.size == 0; + } + + public boolean contains(Object var1) { + return this.indexOf(var1) >= 0; + } + + public int indexOf(Object var1) { + return this.indexOfRange(var1, 0, this.size); + } + + int indexOfRange(Object var1, int var2, int var3) { + Object[] var4 = this.elementData; + int var5; + if (var1 == null) { + for(var5 = var2; var5 < var3; ++var5) { + if (var4[var5] == null) { + return var5; + } + } + } else { + for(var5 = var2; var5 < var3; ++var5) { + if (var1.equals(var4[var5])) { + return var5; + } + } + } + + return -1; + } + + public int lastIndexOf(Object var1) { + return this.lastIndexOfRange(var1, 0, this.size); + } + + int lastIndexOfRange(Object var1, int var2, int var3) { + Object[] var4 = this.elementData; + int var5; + if (var1 == null) { + for(var5 = var3 - 1; var5 >= var2; --var5) { + if (var4[var5] == null) { + return var5; + } + } + } else { + for(var5 = var3 - 1; var5 >= var2; --var5) { + if (var1.equals(var4[var5])) { + return var5; + } + } + } + + return -1; + } + + public Object clone() { + try { + ArrayList var1 = (ArrayList)super.clone(); + var1.elementData = Arrays.copyOf(this.elementData, this.size); + var1.modCount = 0; + return var1; + } catch (CloneNotSupportedException var2) { + throw new InternalError(var2); + } + } + + public Object[] toArray() { + return Arrays.copyOf(this.elementData, this.size); + } + + public T[] toArray(T[] var1) { + if (var1.length < this.size) { + return Arrays.copyOf(this.elementData, this.size, var1.getClass()); + } else { + System.arraycopy(this.elementData, 0, var1, 0, this.size); + if (var1.length > this.size) { + var1[this.size] = null; + } + + return var1; + } + } + + E elementData(int var1) { + return this.elementData[var1]; + } + + static E elementAt(Object[] var0, int var1) { + return var0[var1]; + } + + public E get(int var1) { + Objects.checkIndex(var1, this.size); + return this.elementData(var1); + } + + public E set(int var1, E var2) { + Objects.checkIndex(var1, this.size); + Object var3 = this.elementData(var1); + this.elementData[var1] = var2; + return var3; + } + + private void add(E var1, Object[] var2, int var3) { + if (var3 == var2.length) { + var2 = this.grow(); + } + + var2[var3] = var1; + this.size = var3 + 1; + } + + public boolean add(E var1) { + ++this.modCount; + this.add(var1, this.elementData, this.size); + return true; + } + + public void add(int var1, E var2) { + this.rangeCheckForAdd(var1); + ++this.modCount; + int var3; + Object[] var4; + if ((var3 = this.size) == (var4 = this.elementData).length) { + var4 = this.grow(); + } + + System.arraycopy(var4, var1, var4, var1 + 1, var3 - var1); + var4[var1] = var2; + this.size = var3 + 1; + } + + public E remove(int var1) { + Objects.checkIndex(var1, this.size); + Object[] var2 = this.elementData; + Object var3 = var2[var1]; + this.fastRemove(var2, var1); + return var3; + } + + public boolean equals(Object var1) { + if (var1 == this) { + return true; + } else if (!(var1 instanceof List)) { + return false; + } else { + int var2 = this.modCount; + boolean var3 = var1.getClass() == ArrayList.class ? this.equalsArrayList((ArrayList)var1) : this.equalsRange((List)var1, 0, this.size); + this.checkForComodification(var2); + return var3; + } + } + + boolean equalsRange(List var1, int var2, int var3) { + Object[] var4 = this.elementData; + if (var3 > var4.length) { + throw new ConcurrentModificationException(); + } else { + Iterator var5; + for(var5 = var1.iterator(); var2 < var3; ++var2) { + if (!var5.hasNext() || !Objects.equals(var4[var2], var5.next())) { + return false; + } + } + + return !var5.hasNext(); + } + } + + private boolean equalsArrayList(ArrayList var1) { + int var2 = var1.modCount; + int var3 = this.size; + boolean var4; + if (var4 = var3 == var1.size) { + Object[] var5 = var1.elementData; + Object[] var6 = this.elementData; + if (var3 > var6.length || var3 > var5.length) { + throw new ConcurrentModificationException(); + } + + for(int var7 = 0; var7 < var3; ++var7) { + if (!Objects.equals(var6[var7], var5[var7])) { + var4 = false; + break; + } + } + } + + var1.checkForComodification(var2); + return var4; + } + + private void checkForComodification(int var1) { + if (this.modCount != var1) { + throw new ConcurrentModificationException(); + } + } + + public int hashCode() { + int var1 = this.modCount; + int var2 = this.hashCodeRange(0, this.size); + this.checkForComodification(var1); + return var2; + } + + int hashCodeRange(int var1, int var2) { + Object[] var3 = this.elementData; + if (var2 > var3.length) { + throw new ConcurrentModificationException(); + } else { + int var4 = 1; + + for(int var5 = var1; var5 < var2; ++var5) { + Object var6 = var3[var5]; + var4 = 31 * var4 + (var6 == null ? 0 : var6.hashCode()); + } + + return var4; + } + } + + public boolean remove(Object var1) { + Object[] var2 = this.elementData; + int var3 = this.size; + int var4 = 0; + if (var1 == null) { + while(true) { + if (var4 >= var3) { + return false; + } + + if (var2[var4] == null) { + break; + } + + ++var4; + } + } else { + while(true) { + if (var4 >= var3) { + return false; + } + + if (var1.equals(var2[var4])) { + break; + } + + ++var4; + } + } + + this.fastRemove(var2, var4); + return true; + } + + private void fastRemove(Object[] var1, int var2) { + ++this.modCount; + int var3; + if ((var3 = this.size - 1) > var2) { + System.arraycopy(var1, var2 + 1, var1, var2, var3 - var2); + } + + var1[this.size = var3] = null; + } + + public void clear() { + ++this.modCount; + Object[] var1 = this.elementData; + int var2 = this.size; + + for(int var3 = this.size = 0; var3 < var2; ++var3) { + var1[var3] = null; + } + + } + + public boolean addAll(Collection var1) { + Object[] var2 = var1.toArray(); + ++this.modCount; + int var3 = var2.length; + if (var3 == 0) { + return false; + } else { + Object[] var4; + int var5; + if (var3 > (var4 = this.elementData).length - (var5 = this.size)) { + var4 = this.grow(var5 + var3); + } + + System.arraycopy(var2, 0, var4, var5, var3); + this.size = var5 + var3; + return true; + } + } + + public boolean addAll(int var1, Collection var2) { + this.rangeCheckForAdd(var1); + Object[] var3 = var2.toArray(); + ++this.modCount; + int var4 = var3.length; + if (var4 == 0) { + return false; + } else { + Object[] var5; + int var6; + if (var4 > (var5 = this.elementData).length - (var6 = this.size)) { + var5 = this.grow(var6 + var4); + } + + int var7 = var6 - var1; + if (var7 > 0) { + System.arraycopy(var5, var1, var5, var1 + var4, var7); + } + + System.arraycopy(var3, 0, var5, var1, var4); + this.size = var6 + var4; + return true; + } + } + + protected void removeRange(int var1, int var2) { + if (var1 > var2) { + throw new IndexOutOfBoundsException(outOfBoundsMsg(var1, var2)); + } else { + ++this.modCount; + this.shiftTailOverGap(this.elementData, var1, var2); + } + } + + private void shiftTailOverGap(Object[] var1, int var2, int var3) { + System.arraycopy(var1, var3, var1, var2, this.size - var3); + int var4 = this.size; + + for(int var5 = this.size -= var3 - var2; var5 < var4; ++var5) { + var1[var5] = null; + } + + } + + private void rangeCheckForAdd(int var1) { + if (var1 > this.size || var1 < 0) { + throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1)); + } + } + + private String outOfBoundsMsg(int var1) { + return "Index: " + var1 + ", Size: " + this.size; + } + + private static String outOfBoundsMsg(int var0, int var1) { + return "From Index: " + var0 + " > To Index: " + var1; + } + + public boolean removeAll(Collection var1) { + return this.batchRemove(var1, false, 0, this.size); + } + + public boolean retainAll(Collection var1) { + return this.batchRemove(var1, true, 0, this.size); + } + + boolean batchRemove(Collection var1, boolean var2, int var3, int var4) { + Objects.requireNonNull(var1); + Object[] var5 = this.elementData; + + for(int var6 = var3; var6 != var4; ++var6) { + if (var1.contains(var5[var6]) != var2) { + int var7 = var6++; + + try { + for(; var6 < var4; ++var6) { + Object var8; + if (var1.contains(var8 = var5[var6]) == var2) { + var5[var7++] = var8; + } + } + } catch (Throwable var12) { + System.arraycopy(var5, var6, var5, var7, var4 - var6); + var7 += var4 - var6; + throw var12; + } finally { + this.modCount += var4 - var7; + this.shiftTailOverGap(var5, var7, var4); + } + + return true; + } + } + + return false; + } + + private void writeObject(ObjectOutputStream var1) throws IOException { + int var2 = this.modCount; + var1.defaultWriteObject(); + var1.writeInt(this.size); + + for(int var3 = 0; var3 < this.size; ++var3) { + var1.writeObject(this.elementData[var3]); + } + + if (this.modCount != var2) { + throw new ConcurrentModificationException(); + } + } + + private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { + var1.defaultReadObject(); + var1.readInt(); + if (this.size > 0) { + SharedSecrets.getJavaObjectInputStreamAccess().checkArray(var1, Object[].class, this.size); + Object[] var2 = new Object[this.size]; + + for(int var3 = 0; var3 < this.size; ++var3) { + var2[var3] = var1.readObject(); + } + + this.elementData = var2; + } else { + if (this.size != 0) { + throw new InvalidObjectException("Invalid size: " + this.size); + } + + this.elementData = EMPTY_ELEMENTDATA; + } + + } + + public ListIterator listIterator(int var1) { + this.rangeCheckForAdd(var1); + return new ListItr(var1); + } + + public ListIterator listIterator() { + return new ListItr(0); + } + + public Iterator iterator() { + return new Itr(); + } + + public List subList(int var1, int var2) { + subListRangeCheck(var1, var2, this.size); + return new SubList(this, var1, var2); + } + + public void forEach(Consumer var1) { + Objects.requireNonNull(var1); + int var2 = this.modCount; + Object[] var3 = this.elementData; + int var4 = this.size; + + for(int var5 = 0; this.modCount == var2 && var5 < var4; ++var5) { + var1.accept(elementAt(var3, var5)); + } + + if (this.modCount != var2) { + throw new ConcurrentModificationException(); + } + } + + public Spliterator spliterator() { + return new ArrayListSpliterator(0, -1, 0); + } + + private static long[] nBits(int var0) { + return new long[(var0 - 1 >> 6) + 1]; + } + + private static void setBit(long[] var0, int var1) { + var0[var1 >> 6] |= 1L << var1; + } + + private static boolean isClear(long[] var0, int var1) { + return (var0[var1 >> 6] & 1L << var1) == 0L; + } + + public boolean removeIf(Predicate var1) { + return this.removeIf(var1, 0, this.size); + } + + boolean removeIf(Predicate var1, int var2, int var3) { + Objects.requireNonNull(var1); + int var4 = this.modCount; + + Object[] var5; + for(var5 = this.elementData; var2 < var3 && !var1.test(elementAt(var5, var2)); ++var2) { + } + + if (var2 < var3) { + int var6 = var2; + long[] var7 = nBits(var3 - var2); + var7[0] = 1L; + ++var2; + + for(; var2 < var3; ++var2) { + if (var1.test(elementAt(var5, var2))) { + setBit(var7, var2 - var6); + } + } + + if (this.modCount != var4) { + throw new ConcurrentModificationException(); + } else { + ++this.modCount; + int var8 = var6; + + for(var2 = var6; var2 < var3; ++var2) { + if (isClear(var7, var2 - var6)) { + var5[var8++] = var5[var2]; + } + } + + this.shiftTailOverGap(var5, var8, var3); + return true; + } + } else if (this.modCount != var4) { + throw new ConcurrentModificationException(); + } else { + return false; + } + } + + public void replaceAll(UnaryOperator var1) { + this.replaceAllRange(var1, 0, this.size); + ++this.modCount; + } + + private void replaceAllRange(UnaryOperator var1, int var2, int var3) { + Objects.requireNonNull(var1); + int var4 = this.modCount; + + for(Object[] var5 = this.elementData; this.modCount == var4 && var2 < var3; ++var2) { + var5[var2] = var1.apply(elementAt(var5, var2)); + } + + if (this.modCount != var4) { + throw new ConcurrentModificationException(); + } + } + + public void sort(Comparator var1) { + int var2 = this.modCount; + Arrays.sort(this.elementData, 0, this.size, var1); + if (this.modCount != var2) { + throw new ConcurrentModificationException(); + } else { + ++this.modCount; + } + } + + void checkInvariants() { + } + + private class ListItr extends ArrayList.Itr implements ListIterator { + ListItr(int var2) { + super(); + this.cursor = var2; + } + + public boolean hasPrevious() { + return this.cursor != 0; + } + + public int nextIndex() { + return this.cursor; + } + + public int previousIndex() { + return this.cursor - 1; + } + + public E previous() { + this.checkForComodification(); + int var1 = this.cursor - 1; + if (var1 < 0) { + throw new NoSuchElementException(); + } else { + Object[] var2 = ArrayList.this.elementData; + if (var1 >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1; + return var2[this.lastRet = var1]; + } + } + } + + public void set(E var1) { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + ArrayList.this.set(this.lastRet, var1); + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + } + + public void add(E var1) { + this.checkForComodification(); + + try { + int var2 = this.cursor; + ArrayList.this.add(var2, var1); + this.cursor = var2 + 1; + this.lastRet = -1; + this.expectedModCount = ArrayList.this.modCount; + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + } + + private class Itr implements Iterator { + int cursor; + int lastRet = -1; + int expectedModCount; + + Itr() { + this.expectedModCount = ArrayList.this.modCount; + } + + public boolean hasNext() { + return this.cursor != ArrayList.this.size; + } + + public E next() { + this.checkForComodification(); + int var1 = this.cursor; + if (var1 >= ArrayList.this.size) { + throw new NoSuchElementException(); + } else { + Object[] var2 = ArrayList.this.elementData; + if (var1 >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1 + 1; + return var2[this.lastRet = var1]; + } + } + } + + public void remove() { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + ArrayList.this.remove(this.lastRet); + this.cursor = this.lastRet; + this.lastRet = -1; + this.expectedModCount = ArrayList.this.modCount; + } catch (IndexOutOfBoundsException var2) { + throw new ConcurrentModificationException(); + } + } + } + + public void forEachRemaining(Consumer var1) { + Objects.requireNonNull(var1); + int var2 = ArrayList.this.size; + int var3 = this.cursor; + if (var3 < var2) { + Object[] var4 = ArrayList.this.elementData; + if (var3 >= var4.length) { + throw new ConcurrentModificationException(); + } + + while(var3 < var2 && ArrayList.this.modCount == this.expectedModCount) { + var1.accept(ArrayList.elementAt(var4, var3)); + ++var3; + } + + this.cursor = var3; + this.lastRet = var3 - 1; + this.checkForComodification(); + } + + } + + final void checkForComodification() { + if (ArrayList.this.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + private static class SubList extends AbstractList implements RandomAccess { + private final ArrayList root; + private final SubList parent; + private final int offset; + private int size; + + public SubList(ArrayList var1, int var2, int var3) { + this.root = var1; + this.parent = null; + this.offset = var2; + this.size = var3 - var2; + this.modCount = var1.modCount; + } + + private SubList(SubList var1, int var2, int var3) { + this.root = var1.root; + this.parent = var1; + this.offset = var1.offset + var2; + this.size = var3 - var2; + this.modCount = var1.modCount; + } + + public E set(int var1, E var2) { + Objects.checkIndex(var1, this.size); + this.checkForComodification(); + Object var3 = this.root.elementData(this.offset + var1); + this.root.elementData[this.offset + var1] = var2; + return var3; + } + + public E get(int var1) { + Objects.checkIndex(var1, this.size); + this.checkForComodification(); + return this.root.elementData(this.offset + var1); + } + + public int size() { + this.checkForComodification(); + return this.size; + } + + public void add(int var1, E var2) { + this.rangeCheckForAdd(var1); + this.checkForComodification(); + this.root.add(this.offset + var1, var2); + this.updateSizeAndModCount(1); + } + + public E remove(int var1) { + Objects.checkIndex(var1, this.size); + this.checkForComodification(); + Object var2 = this.root.remove(this.offset + var1); + this.updateSizeAndModCount(-1); + return var2; + } + + protected void removeRange(int var1, int var2) { + this.checkForComodification(); + this.root.removeRange(this.offset + var1, this.offset + var2); + this.updateSizeAndModCount(var1 - var2); + } + + public boolean addAll(Collection var1) { + return this.addAll(this.size, var1); + } + + public boolean addAll(int var1, Collection var2) { + this.rangeCheckForAdd(var1); + int var3 = var2.size(); + if (var3 == 0) { + return false; + } else { + this.checkForComodification(); + this.root.addAll(this.offset + var1, var2); + this.updateSizeAndModCount(var3); + return true; + } + } + + public void replaceAll(UnaryOperator var1) { + this.root.replaceAllRange(var1, this.offset, this.offset + this.size); + } + + public boolean removeAll(Collection var1) { + return this.batchRemove(var1, false); + } + + public boolean retainAll(Collection var1) { + return this.batchRemove(var1, true); + } + + private boolean batchRemove(Collection var1, boolean var2) { + this.checkForComodification(); + int var3 = this.root.size; + boolean var4 = this.root.batchRemove(var1, var2, this.offset, this.offset + this.size); + if (var4) { + this.updateSizeAndModCount(this.root.size - var3); + } + + return var4; + } + + public boolean removeIf(Predicate var1) { + this.checkForComodification(); + int var2 = this.root.size; + boolean var3 = this.root.removeIf(var1, this.offset, this.offset + this.size); + if (var3) { + this.updateSizeAndModCount(this.root.size - var2); + } + + return var3; + } + + public Object[] toArray() { + this.checkForComodification(); + return Arrays.copyOfRange(this.root.elementData, this.offset, this.offset + this.size); + } + + public T[] toArray(T[] var1) { + this.checkForComodification(); + if (var1.length < this.size) { + return Arrays.copyOfRange(this.root.elementData, this.offset, this.offset + this.size, var1.getClass()); + } else { + System.arraycopy(this.root.elementData, this.offset, var1, 0, this.size); + if (var1.length > this.size) { + var1[this.size] = null; + } + + return var1; + } + } + + public boolean equals(Object var1) { + if (var1 == this) { + return true; + } else if (!(var1 instanceof List)) { + return false; + } else { + boolean var2 = this.root.equalsRange((List)var1, this.offset, this.offset + this.size); + this.checkForComodification(); + return var2; + } + } + + public int hashCode() { + int var1 = this.root.hashCodeRange(this.offset, this.offset + this.size); + this.checkForComodification(); + return var1; + } + + public int indexOf(Object var1) { + int var2 = this.root.indexOfRange(var1, this.offset, this.offset + this.size); + this.checkForComodification(); + return var2 >= 0 ? var2 - this.offset : -1; + } + + public int lastIndexOf(Object var1) { + int var2 = this.root.lastIndexOfRange(var1, this.offset, this.offset + this.size); + this.checkForComodification(); + return var2 >= 0 ? var2 - this.offset : -1; + } + + public boolean contains(Object var1) { + return this.indexOf(var1) >= 0; + } + + public Iterator iterator() { + return this.listIterator(); + } + + public ListIterator listIterator(final int var1) { + this.checkForComodification(); + this.rangeCheckForAdd(var1); + return new ListIterator() { + int cursor = var1; + int lastRet = -1; + int expectedModCount; + + { + this.expectedModCount = SubList.this.modCount; + } + + public boolean hasNext() { + return this.cursor != SubList.this.size; + } + + public E next() { + this.checkForComodification(); + int var1x = this.cursor; + if (var1x >= SubList.this.size) { + throw new NoSuchElementException(); + } else { + Object[] var2 = SubList.this.root.elementData; + if (SubList.this.offset + var1x >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1x + 1; + return var2[SubList.this.offset + (this.lastRet = var1x)]; + } + } + } + + public boolean hasPrevious() { + return this.cursor != 0; + } + + public E previous() { + this.checkForComodification(); + int var1x = this.cursor - 1; + if (var1x < 0) { + throw new NoSuchElementException(); + } else { + Object[] var2 = SubList.this.root.elementData; + if (SubList.this.offset + var1x >= var2.length) { + throw new ConcurrentModificationException(); + } else { + this.cursor = var1x; + return var2[SubList.this.offset + (this.lastRet = var1x)]; + } + } + } + + public void forEachRemaining(Consumer var1x) { + Objects.requireNonNull(var1x); + int var2 = SubList.this.size; + int var3 = this.cursor; + if (var3 < var2) { + Object[] var4 = SubList.this.root.elementData; + if (SubList.this.offset + var3 >= var4.length) { + throw new ConcurrentModificationException(); + } + + while(var3 < var2 && SubList.this.root.modCount == this.expectedModCount) { + var1x.accept(ArrayList.elementAt(var4, SubList.this.offset + var3)); + ++var3; + } + + this.cursor = var3; + this.lastRet = var3 - 1; + this.checkForComodification(); + } + + } + + public int nextIndex() { + return this.cursor; + } + + public int previousIndex() { + return this.cursor - 1; + } + + public void remove() { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + SubList.this.remove(this.lastRet); + this.cursor = this.lastRet; + this.lastRet = -1; + this.expectedModCount = SubList.this.modCount; + } catch (IndexOutOfBoundsException var2) { + throw new ConcurrentModificationException(); + } + } + } + + public void set(E var1x) { + if (this.lastRet < 0) { + throw new IllegalStateException(); + } else { + this.checkForComodification(); + + try { + SubList.this.root.set(SubList.this.offset + this.lastRet, var1x); + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + } + + public void add(E var1x) { + this.checkForComodification(); + + try { + int var2 = this.cursor; + SubList.this.add(var2, var1x); + this.cursor = var2 + 1; + this.lastRet = -1; + this.expectedModCount = SubList.this.modCount; + } catch (IndexOutOfBoundsException var3) { + throw new ConcurrentModificationException(); + } + } + + final void checkForComodification() { + if (SubList.this.root.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } + } + }; + } + + public List subList(int var1, int var2) { + subListRangeCheck(var1, var2, this.size); + return new SubList(this, var1, var2); + } + + private void rangeCheckForAdd(int var1) { + if (var1 < 0 || var1 > this.size) { + throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1)); + } + } + + private String outOfBoundsMsg(int var1) { + return "Index: " + var1 + ", Size: " + this.size; + } + + private void checkForComodification() { + if (this.root.modCount != this.modCount) { + throw new ConcurrentModificationException(); + } + } + + private void updateSizeAndModCount(int var1) { + SubList var2 = this; + + do { + var2.size += var1; + var2.modCount = this.root.modCount; + var2 = var2.parent; + } while(var2 != null); + + } + + public Spliterator spliterator() { + this.checkForComodification(); + return new Spliterator() { + private int index; + private int fence; + private int expectedModCount; + + { + this.index = SubList.this.offset; + this.fence = -1; + } + + private int getFence() { + int var1; + if ((var1 = this.fence) < 0) { + this.expectedModCount = SubList.this.modCount; + var1 = this.fence = SubList.this.offset + SubList.this.size; + } + + return var1; + } + + public ArrayList.ArrayListSpliterator trySplit() { + int var1 = this.getFence(); + int var2 = this.index; + int var3 = var2 + var1 >>> 1; + ArrayListSpliterator var10000; + if (var2 >= var3) { + var10000 = null; + } else { + ArrayList var10002 = SubList.this.root; + Objects.requireNonNull(var10002); + var10000 = var10002.new ArrayListSpliterator(var2, this.index = var3, this.expectedModCount); + } + + return var10000; + } + + public boolean tryAdvance(Consumer var1) { + Objects.requireNonNull(var1); + int var2 = this.getFence(); + int var3 = this.index; + if (var3 < var2) { + this.index = var3 + 1; + Object var4 = SubList.this.root.elementData[var3]; + var1.accept(var4); + if (SubList.this.root.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } else { + return true; + } + } else { + return false; + } + } + + public void forEachRemaining(Consumer var1) { + Objects.requireNonNull(var1); + ArrayList var5 = SubList.this.root; + Object[] var6; + if ((var6 = var5.elementData) != null) { + int var3; + int var4; + if ((var3 = this.fence) < 0) { + var4 = SubList.this.modCount; + var3 = SubList.this.offset + SubList.this.size; + } else { + var4 = this.expectedModCount; + } + + int var2; + if ((var2 = this.index) >= 0 && (this.index = var3) <= var6.length) { + while(var2 < var3) { + Object var7 = var6[var2]; + var1.accept(var7); + ++var2; + } + + if (var5.modCount == var4) { + return; + } + } + } + + throw new ConcurrentModificationException(); + } + + public long estimateSize() { + return (long)(this.getFence() - this.index); + } + + public int characteristics() { + return 16464; + } + }; + } + } + + final class ArrayListSpliterator implements Spliterator { + private int index; + private int fence; + private int expectedModCount; + + ArrayListSpliterator(int var2, int var3, int var4) { + this.index = var2; + this.fence = var3; + this.expectedModCount = var4; + } + + private int getFence() { + int var1; + if ((var1 = this.fence) < 0) { + this.expectedModCount = ArrayList.this.modCount; + var1 = this.fence = ArrayList.this.size; + } + + return var1; + } + + public ArrayList.ArrayListSpliterator trySplit() { + int var1 = this.getFence(); + int var2 = this.index; + int var3 = var2 + var1 >>> 1; + return var2 >= var3 ? null : ArrayList.this.new ArrayListSpliterator(var2, this.index = var3, this.expectedModCount); + } + + public boolean tryAdvance(Consumer var1) { + if (var1 == null) { + throw new NullPointerException(); + } else { + int var2 = this.getFence(); + int var3 = this.index; + if (var3 < var2) { + this.index = var3 + 1; + Object var4 = ArrayList.this.elementData[var3]; + var1.accept(var4); + if (ArrayList.this.modCount != this.expectedModCount) { + throw new ConcurrentModificationException(); + } else { + return true; + } + } else { + return false; + } + } + } + + public void forEachRemaining(Consumer var1) { + if (var1 == null) { + throw new NullPointerException(); + } else { + Object[] var5; + if ((var5 = ArrayList.this.elementData) != null) { + int var3; + int var4; + if ((var3 = this.fence) < 0) { + var4 = ArrayList.this.modCount; + var3 = ArrayList.this.size; + } else { + var4 = this.expectedModCount; + } + + int var2; + if ((var2 = this.index) >= 0 && (this.index = var3) <= var5.length) { + while(var2 < var3) { + Object var6 = var5[var2]; + var1.accept(var6); + ++var2; + } + + if (ArrayList.this.modCount == var4) { + return; + } + } + } + + throw new ConcurrentModificationException(); + } + } + + public long estimateSize() { + return (long)(this.getFence() - this.index); + } + + public int characteristics() { + return 16464; + } + } + } + """.trimIndent() diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index e80408d..ab90013 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -38,7 +38,7 @@ class HighlightsTest { @Test fun `returns list of code highlights`() { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } fun `returns different keywords for default builder`() { val instance = Highlights.default() @@ -54,7 +54,7 @@ class HighlightsTest { @Test fun `returns null job before first invocation`() { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } fun `returns different keywords for default manual builder`() { val instance = Highlights.Builder().build() @@ -66,7 +66,7 @@ class HighlightsTest { @Test fun `returns active job after start`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } val highlights = instance.getHighlights() @@ -81,7 +81,7 @@ class HighlightsTest { @Test fun `returns inactive job after completion`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } suspendCancellableCoroutine { continuation -> @@ -98,7 +98,7 @@ class HighlightsTest { @Test fun `returns error for exception during analysis`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } var error: Throwable? = null @@ -120,7 +120,7 @@ class HighlightsTest { @Test fun `cancels first analysis when second is invoked`() { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } fun `returns highlights after code change`() { val instance = Highlights.default() @@ -150,7 +150,7 @@ class HighlightsTest { @Test fun `returns active job for each invocation`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } val defaultJob = Job(null) as Job @@ -185,7 +185,7 @@ class HighlightsTest { @Test fun `returns list of code highlights asynchronously`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } val result = suspendCancellableCoroutine { continuation -> @@ -200,7 +200,7 @@ class HighlightsTest { @Test fun `returns asynchronous results one by one`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } var result1: List @@ -214,7 +214,7 @@ class HighlightsTest { println("Time1: ${time1.inWholeMilliseconds} ms") assertTrue { result1.isNotEmpty() } - default.setCode(longTestCode().replace("static", "statac")) + default.setCode(longJavaCode.replace("static", "statac")) var result2: List val time2 = measureTime { @@ -233,7 +233,7 @@ class HighlightsTest { @Test fun `returns immediately result from second invocation`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } var time1: Duration @@ -263,7 +263,7 @@ class HighlightsTest { @Test fun `returns cancellation result`() = runTest { val default = Highlights.default().apply { - setCode(longTestCode()) + setCode(longJavaCode) } var cancelled = false From 374af374249410110020a09d92569b401a6d845b Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Sun, 22 Sep 2024 11:58:28 +0200 Subject: [PATCH 07/11] Removed wrong gitignore rule --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0881554..b60b6c6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,4 @@ gradle-app.setting # JDT-specific (Eclipse Java Development Tools) .classpath # MacOS files -*.DS_Store -/sample/*.DS_Store +*.DS_Store \ No newline at end of file From 8ddbf420e95cf4d2ebe669baf209047bb4995060 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Sun, 22 Sep 2024 12:00:45 +0200 Subject: [PATCH 08/11] Fixed example indents --- .gitignore | 2 +- README.md | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index b60b6c6..5b6c852 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ gradle-app.setting # JDT-specific (Eclipse Java Development Tools) .classpath # MacOS files -*.DS_Store \ No newline at end of file +*.DS_Store diff --git a/README.md b/README.md index 0953e3a..15cc82d 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,16 @@ Highlights.default().apply { There is also a possibility to handle result asynchronously ```kotlin - highlights.getHighlightsAsync( - object : DefaultHighlightsResultListener() { - // onStart - // onError - // onCancel - override fun onComplete(highlights: List) { - emitResult(highlights) - } +highlights.getHighlightsAsync( + object : DefaultHighlightsResultListener() { + // onStart + // onError + // onCancel + override fun onComplete(highlights: List) { + emitResult(highlights) } - ) + } +) ``` You can also set language, theme and phrase emphasis. @@ -319,7 +319,7 @@ If your project uses this code, please write me or add your info ## TODO 🚧 - [X] Migrate some lists to sets -- [ ] Optimize code analysis +- [X] Optimize code analysis - [ ] Add more themes and languages - [ ] Support italic and underline text style From 7359ca700c28312c61c3159bbbbdf95b8f9db8e8 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Thu, 26 Sep 2024 08:00:02 +0200 Subject: [PATCH 09/11] Removed Job field from public API --- .../dev/snipme/highlights/Highlights.kt | 5 +- .../highlights/internal/HighlightsTest.kt | 1473 +---------------- 2 files changed, 14 insertions(+), 1464 deletions(-) diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index c3656bc..5eaf521 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -23,10 +23,9 @@ class Highlights private constructor( private val theme: SyntaxTheme, private var emphasisLocations: List ) { - var snapshot: CodeSnapshot? = null - private set + private var analysisJob: Job? = null - var analysisJob: Job? = null + var snapshot: CodeSnapshot? = null private set companion object { diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index ab90013..065f24a 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -3,10 +3,8 @@ package dev.snipme.highlights.internal import dev.snipme.highlights.Highlights import dev.snipme.highlights.HighlightsResultListener import dev.snipme.highlights.model.CodeHighlight -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.test.StandardTestDispatcher @@ -36,63 +34,13 @@ class HighlightsTest { } @Test - fun `returns list of code highlights`() { + fun `returns list of code highlights for sync call`() { val default = Highlights.default().apply { setCode(longJavaCode) } - fun `returns different keywords for default builder`() { - val instance = Highlights.default() - instance.setCode("class ") val highlights = default.getHighlights() assertTrue { highlights.isNotEmpty() } - val highlights = instance.getHighlights() - - assertTrue(highlights.isNotEmpty()) - } - - @Test - fun `returns null job before first invocation`() { - val default = Highlights.default().apply { - setCode(longJavaCode) - } - fun `returns different keywords for default manual builder`() { - val instance = Highlights.Builder().build() - instance.setCode("class ") - - assertTrue { default.analysisJob == null } - } - - @Test - fun `returns active job after start`() = runTest { - val default = Highlights.default().apply { - setCode(longJavaCode) - } - val highlights = instance.getHighlights() - - suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest(default, onStart = { - assertTrue { default.analysisJob?.isActive == true } - continuation.resume(Unit) {} - }) - } - } - - @Test - fun `returns inactive job after completion`() = runTest { - val default = Highlights.default().apply { - setCode(longJavaCode) - } - - suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest(default, - onStart = { - default.analysisJob?.invokeOnCompletion { - assertTrue { default.analysisJob?.isActive == false } - } - } - ) { continuation.resume(Unit) {} } - } } @Test @@ -114,72 +62,27 @@ class HighlightsTest { } assertTrue { error != null } - assertTrue(highlights.isNotEmpty()) } @Test - fun `cancels first analysis when second is invoked`() { + fun `cancels first analysis when second is invoked`() = runTest { val default = Highlights.default().apply { setCode(longJavaCode) } - fun `returns highlights after code change`() { - val instance = Highlights.default() - - val highlights = instance.getHighlights() - - assertTrue(highlights.isEmpty()) - - instance.setCode("class ") - var job1: Job? - invokeHighlightsRequest( - default, - onStart = { - job1 = default.analysisJob - invokeHighlightsRequest(default, onStart = { - assertTrue { job1?.isActive == false } - assertTrue { default.analysisJob?.isActive == true } - }) - }, - ) - val newHighlights = instance.getHighlights() - - assertTrue(newHighlights.isNotEmpty()) - } - - @Test - fun `returns active job for each invocation`() = runTest { - val default = Highlights.default().apply { - setCode(longJavaCode) - } - - val defaultJob = Job(null) as Job - - var job1 = defaultJob - suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest(default, - onStart = { - job1 = default.analysisJob!! - assertTrue { job1.isActive } - }, - onCompleted = { - continuation.resume(it) {} - }) - } - - var job2 = defaultJob + var wasCancelled = false suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest(default, + invokeHighlightsRequest( + default, + onCancel = { wasCancelled = true }, onStart = { - job2 = default.analysisJob!! - assertTrue { job2.isActive } + invokeHighlightsRequest(default) { + assertTrue { wasCancelled } + continuation.resume(Unit) {} + } }, - onCompleted = { - continuation.resume(it) {} - }) + ) } - - assertTrue { job1 != job2 } } @Test @@ -260,26 +163,6 @@ class HighlightsTest { } } - @Test - fun `returns cancellation result`() = runTest { - val default = Highlights.default().apply { - setCode(longJavaCode) - } - - var cancelled = false - suspendCancellableCoroutine { continuation -> - invokeHighlightsRequest(default, - onStart = { default.analysisJob?.cancel() }, - onCancel = { - cancelled = true - continuation.resume(Unit) {} - } - ) - } - - assertTrue { cancelled } - } - private fun invokeAndMeasureTime( highlights: Highlights, onFinish: (Duration) -> Unit = {} @@ -294,17 +177,9 @@ class HighlightsTest { } } - fun listen() { - highlights.analysisJob?.invokeOnCompletion { - if (it is CancellationException) { - updateFirstTime() - } - } - } - invokeHighlightsRequest( highlights, - onStart = { println("Start"); listen() }, + onStart = { println("Start"); }, onCancel = { println("Cancel"); updateFirstTime() }, onError = { println("Error: $it"); updateFirstTime() }, onCompleted = { println("Completed"); updateFirstTime() }, @@ -325,1328 +200,4 @@ class HighlightsTest { override fun onCancel() = onCancel() }) } - - private fun longTestCode() = """ - // - // Source code recreated from a .class file by IntelliJ IDEA - // (powered by FernFlower decompiler) - // - - package java.util; - - import java.io.IOException; - import java.io.InvalidObjectException; - import java.io.ObjectInputStream; - import java.io.ObjectOutputStream; - import java.io.Serializable; - import java.util.function.Consumer; - import java.util.function.Predicate; - import java.util.function.UnaryOperator; - import jdk.internal.access.SharedSecrets; - import jdk.internal.util.ArraysSupport; - - public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable { - private static final long serialVersionUID = 8683452581122892189L; - private static final int DEFAULT_CAPACITY = 10; - private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; - transient Object[] elementData; - private int size; - - public ArrayList(int var1) { - if (var1 > 0) { - this.elementData = new Object[var1]; - } else { - if (var1 != 0) { - throw new IllegalArgumentException("Illegal Capacity: " + var1); - } - - this.elementData = EMPTY_ELEMENTDATA; - } - - } - - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - public ArrayList(Collection var1) { - Object[] var2 = var1.toArray(); - if ((this.size = var2.length) != 0) { - if (var1.getClass() == ArrayList.class) { - this.elementData = var2; - } else { - this.elementData = Arrays.copyOf(var2, this.size, Object[].class); - } - } else { - this.elementData = EMPTY_ELEMENTDATA; - } - - } - - public void trimToSize() { - ++this.modCount; - if (this.size < this.elementData.length) { - this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size); - } - - } - - public void ensureCapacity(int var1) { - if (var1 > this.elementData.length && (this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA || var1 > 10)) { - ++this.modCount; - this.grow(var1); - } - - } - - private Object[] grow(int var1) { - int var2 = this.elementData.length; - if (var2 <= 0 && this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - return this.elementData = new Object[Math.max(10, var1)]; - } else { - int var3 = ArraysSupport.newLength(var2, var1 - var2, var2 >> 1); - return this.elementData = Arrays.copyOf(this.elementData, var3); - } - } - - private Object[] grow() { - return this.grow(this.size + 1); - } - - public int size() { - return this.size; - } - - public boolean isEmpty() { - return this.size == 0; - } - - public boolean contains(Object var1) { - return this.indexOf(var1) >= 0; - } - - public int indexOf(Object var1) { - return this.indexOfRange(var1, 0, this.size); - } - - int indexOfRange(Object var1, int var2, int var3) { - Object[] var4 = this.elementData; - int var5; - if (var1 == null) { - for(var5 = var2; var5 < var3; ++var5) { - if (var4[var5] == null) { - return var5; - } - } - } else { - for(var5 = var2; var5 < var3; ++var5) { - if (var1.equals(var4[var5])) { - return var5; - } - } - } - - return -1; - } - - public int lastIndexOf(Object var1) { - return this.lastIndexOfRange(var1, 0, this.size); - } - - int lastIndexOfRange(Object var1, int var2, int var3) { - Object[] var4 = this.elementData; - int var5; - if (var1 == null) { - for(var5 = var3 - 1; var5 >= var2; --var5) { - if (var4[var5] == null) { - return var5; - } - } - } else { - for(var5 = var3 - 1; var5 >= var2; --var5) { - if (var1.equals(var4[var5])) { - return var5; - } - } - } - - return -1; - } - - public Object clone() { - try { - ArrayList var1 = (ArrayList)super.clone(); - var1.elementData = Arrays.copyOf(this.elementData, this.size); - var1.modCount = 0; - return var1; - } catch (CloneNotSupportedException var2) { - throw new InternalError(var2); - } - } - - public Object[] toArray() { - return Arrays.copyOf(this.elementData, this.size); - } - - public T[] toArray(T[] var1) { - if (var1.length < this.size) { - return Arrays.copyOf(this.elementData, this.size, var1.getClass()); - } else { - System.arraycopy(this.elementData, 0, var1, 0, this.size); - if (var1.length > this.size) { - var1[this.size] = null; - } - - return var1; - } - } - - E elementData(int var1) { - return this.elementData[var1]; - } - - static E elementAt(Object[] var0, int var1) { - return var0[var1]; - } - - public E get(int var1) { - Objects.checkIndex(var1, this.size); - return this.elementData(var1); - } - - public E set(int var1, E var2) { - Objects.checkIndex(var1, this.size); - Object var3 = this.elementData(var1); - this.elementData[var1] = var2; - return var3; - } - - private void add(E var1, Object[] var2, int var3) { - if (var3 == var2.length) { - var2 = this.grow(); - } - - var2[var3] = var1; - this.size = var3 + 1; - } - - public boolean add(E var1) { - ++this.modCount; - this.add(var1, this.elementData, this.size); - return true; - } - - public void add(int var1, E var2) { - this.rangeCheckForAdd(var1); - ++this.modCount; - int var3; - Object[] var4; - if ((var3 = this.size) == (var4 = this.elementData).length) { - var4 = this.grow(); - } - - System.arraycopy(var4, var1, var4, var1 + 1, var3 - var1); - var4[var1] = var2; - this.size = var3 + 1; - } - - public E remove(int var1) { - Objects.checkIndex(var1, this.size); - Object[] var2 = this.elementData; - Object var3 = var2[var1]; - this.fastRemove(var2, var1); - return var3; - } - - public boolean equals(Object var1) { - if (var1 == this) { - return true; - } else if (!(var1 instanceof List)) { - return false; - } else { - int var2 = this.modCount; - boolean var3 = var1.getClass() == ArrayList.class ? this.equalsArrayList((ArrayList)var1) : this.equalsRange((List)var1, 0, this.size); - this.checkForComodification(var2); - return var3; - } - } - - boolean equalsRange(List var1, int var2, int var3) { - Object[] var4 = this.elementData; - if (var3 > var4.length) { - throw new ConcurrentModificationException(); - } else { - Iterator var5; - for(var5 = var1.iterator(); var2 < var3; ++var2) { - if (!var5.hasNext() || !Objects.equals(var4[var2], var5.next())) { - return false; - } - } - - return !var5.hasNext(); - } - } - - private boolean equalsArrayList(ArrayList var1) { - int var2 = var1.modCount; - int var3 = this.size; - boolean var4; - if (var4 = var3 == var1.size) { - Object[] var5 = var1.elementData; - Object[] var6 = this.elementData; - if (var3 > var6.length || var3 > var5.length) { - throw new ConcurrentModificationException(); - } - - for(int var7 = 0; var7 < var3; ++var7) { - if (!Objects.equals(var6[var7], var5[var7])) { - var4 = false; - break; - } - } - } - - var1.checkForComodification(var2); - return var4; - } - - private void checkForComodification(int var1) { - if (this.modCount != var1) { - throw new ConcurrentModificationException(); - } - } - - public int hashCode() { - int var1 = this.modCount; - int var2 = this.hashCodeRange(0, this.size); - this.checkForComodification(var1); - return var2; - } - - int hashCodeRange(int var1, int var2) { - Object[] var3 = this.elementData; - if (var2 > var3.length) { - throw new ConcurrentModificationException(); - } else { - int var4 = 1; - - for(int var5 = var1; var5 < var2; ++var5) { - Object var6 = var3[var5]; - var4 = 31 * var4 + (var6 == null ? 0 : var6.hashCode()); - } - - return var4; - } - } - - public boolean remove(Object var1) { - Object[] var2 = this.elementData; - int var3 = this.size; - int var4 = 0; - if (var1 == null) { - while(true) { - if (var4 >= var3) { - return false; - } - - if (var2[var4] == null) { - break; - } - - ++var4; - } - } else { - while(true) { - if (var4 >= var3) { - return false; - } - - if (var1.equals(var2[var4])) { - break; - } - - ++var4; - } - } - - this.fastRemove(var2, var4); - return true; - } - - private void fastRemove(Object[] var1, int var2) { - ++this.modCount; - int var3; - if ((var3 = this.size - 1) > var2) { - System.arraycopy(var1, var2 + 1, var1, var2, var3 - var2); - } - - var1[this.size = var3] = null; - } - - public void clear() { - ++this.modCount; - Object[] var1 = this.elementData; - int var2 = this.size; - - for(int var3 = this.size = 0; var3 < var2; ++var3) { - var1[var3] = null; - } - - } - - public boolean addAll(Collection var1) { - Object[] var2 = var1.toArray(); - ++this.modCount; - int var3 = var2.length; - if (var3 == 0) { - return false; - } else { - Object[] var4; - int var5; - if (var3 > (var4 = this.elementData).length - (var5 = this.size)) { - var4 = this.grow(var5 + var3); - } - - System.arraycopy(var2, 0, var4, var5, var3); - this.size = var5 + var3; - return true; - } - } - - public boolean addAll(int var1, Collection var2) { - this.rangeCheckForAdd(var1); - Object[] var3 = var2.toArray(); - ++this.modCount; - int var4 = var3.length; - if (var4 == 0) { - return false; - } else { - Object[] var5; - int var6; - if (var4 > (var5 = this.elementData).length - (var6 = this.size)) { - var5 = this.grow(var6 + var4); - } - - int var7 = var6 - var1; - if (var7 > 0) { - System.arraycopy(var5, var1, var5, var1 + var4, var7); - } - - System.arraycopy(var3, 0, var5, var1, var4); - this.size = var6 + var4; - return true; - } - } - - protected void removeRange(int var1, int var2) { - if (var1 > var2) { - throw new IndexOutOfBoundsException(outOfBoundsMsg(var1, var2)); - } else { - ++this.modCount; - this.shiftTailOverGap(this.elementData, var1, var2); - } - } - - private void shiftTailOverGap(Object[] var1, int var2, int var3) { - System.arraycopy(var1, var3, var1, var2, this.size - var3); - int var4 = this.size; - - for(int var5 = this.size -= var3 - var2; var5 < var4; ++var5) { - var1[var5] = null; - } - - } - - private void rangeCheckForAdd(int var1) { - if (var1 > this.size || var1 < 0) { - throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1)); - } - } - - private String outOfBoundsMsg(int var1) { - return "Index: " + var1 + ", Size: " + this.size; - } - - private static String outOfBoundsMsg(int var0, int var1) { - return "From Index: " + var0 + " > To Index: " + var1; - } - - public boolean removeAll(Collection var1) { - return this.batchRemove(var1, false, 0, this.size); - } - - public boolean retainAll(Collection var1) { - return this.batchRemove(var1, true, 0, this.size); - } - - boolean batchRemove(Collection var1, boolean var2, int var3, int var4) { - Objects.requireNonNull(var1); - Object[] var5 = this.elementData; - - for(int var6 = var3; var6 != var4; ++var6) { - if (var1.contains(var5[var6]) != var2) { - int var7 = var6++; - - try { - for(; var6 < var4; ++var6) { - Object var8; - if (var1.contains(var8 = var5[var6]) == var2) { - var5[var7++] = var8; - } - } - } catch (Throwable var12) { - System.arraycopy(var5, var6, var5, var7, var4 - var6); - var7 += var4 - var6; - throw var12; - } finally { - this.modCount += var4 - var7; - this.shiftTailOverGap(var5, var7, var4); - } - - return true; - } - } - - return false; - } - - private void writeObject(ObjectOutputStream var1) throws IOException { - int var2 = this.modCount; - var1.defaultWriteObject(); - var1.writeInt(this.size); - - for(int var3 = 0; var3 < this.size; ++var3) { - var1.writeObject(this.elementData[var3]); - } - - if (this.modCount != var2) { - throw new ConcurrentModificationException(); - } - } - - private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { - var1.defaultReadObject(); - var1.readInt(); - if (this.size > 0) { - SharedSecrets.getJavaObjectInputStreamAccess().checkArray(var1, Object[].class, this.size); - Object[] var2 = new Object[this.size]; - - for(int var3 = 0; var3 < this.size; ++var3) { - var2[var3] = var1.readObject(); - } - - this.elementData = var2; - } else { - if (this.size != 0) { - throw new InvalidObjectException("Invalid size: " + this.size); - } - - this.elementData = EMPTY_ELEMENTDATA; - } - - } - - public ListIterator listIterator(int var1) { - this.rangeCheckForAdd(var1); - return new ListItr(var1); - } - - public ListIterator listIterator() { - return new ListItr(0); - } - - public Iterator iterator() { - return new Itr(); - } - - public List subList(int var1, int var2) { - subListRangeCheck(var1, var2, this.size); - return new SubList(this, var1, var2); - } - - public void forEach(Consumer var1) { - Objects.requireNonNull(var1); - int var2 = this.modCount; - Object[] var3 = this.elementData; - int var4 = this.size; - - for(int var5 = 0; this.modCount == var2 && var5 < var4; ++var5) { - var1.accept(elementAt(var3, var5)); - } - - if (this.modCount != var2) { - throw new ConcurrentModificationException(); - } - } - - public Spliterator spliterator() { - return new ArrayListSpliterator(0, -1, 0); - } - - private static long[] nBits(int var0) { - return new long[(var0 - 1 >> 6) + 1]; - } - - private static void setBit(long[] var0, int var1) { - var0[var1 >> 6] |= 1L << var1; - } - - private static boolean isClear(long[] var0, int var1) { - return (var0[var1 >> 6] & 1L << var1) == 0L; - } - - public boolean removeIf(Predicate var1) { - return this.removeIf(var1, 0, this.size); - } - - boolean removeIf(Predicate var1, int var2, int var3) { - Objects.requireNonNull(var1); - int var4 = this.modCount; - - Object[] var5; - for(var5 = this.elementData; var2 < var3 && !var1.test(elementAt(var5, var2)); ++var2) { - } - - if (var2 < var3) { - int var6 = var2; - long[] var7 = nBits(var3 - var2); - var7[0] = 1L; - ++var2; - - for(; var2 < var3; ++var2) { - if (var1.test(elementAt(var5, var2))) { - setBit(var7, var2 - var6); - } - } - - if (this.modCount != var4) { - throw new ConcurrentModificationException(); - } else { - ++this.modCount; - int var8 = var6; - - for(var2 = var6; var2 < var3; ++var2) { - if (isClear(var7, var2 - var6)) { - var5[var8++] = var5[var2]; - } - } - - this.shiftTailOverGap(var5, var8, var3); - return true; - } - } else if (this.modCount != var4) { - throw new ConcurrentModificationException(); - } else { - return false; - } - } - - public void replaceAll(UnaryOperator var1) { - this.replaceAllRange(var1, 0, this.size); - ++this.modCount; - } - - private void replaceAllRange(UnaryOperator var1, int var2, int var3) { - Objects.requireNonNull(var1); - int var4 = this.modCount; - - for(Object[] var5 = this.elementData; this.modCount == var4 && var2 < var3; ++var2) { - var5[var2] = var1.apply(elementAt(var5, var2)); - } - - if (this.modCount != var4) { - throw new ConcurrentModificationException(); - } - } - - public void sort(Comparator var1) { - int var2 = this.modCount; - Arrays.sort(this.elementData, 0, this.size, var1); - if (this.modCount != var2) { - throw new ConcurrentModificationException(); - } else { - ++this.modCount; - } - } - - void checkInvariants() { - } - - private class ListItr extends ArrayList.Itr implements ListIterator { - ListItr(int var2) { - super(); - this.cursor = var2; - } - - public boolean hasPrevious() { - return this.cursor != 0; - } - - public int nextIndex() { - return this.cursor; - } - - public int previousIndex() { - return this.cursor - 1; - } - - public E previous() { - this.checkForComodification(); - int var1 = this.cursor - 1; - if (var1 < 0) { - throw new NoSuchElementException(); - } else { - Object[] var2 = ArrayList.this.elementData; - if (var1 >= var2.length) { - throw new ConcurrentModificationException(); - } else { - this.cursor = var1; - return var2[this.lastRet = var1]; - } - } - } - - public void set(E var1) { - if (this.lastRet < 0) { - throw new IllegalStateException(); - } else { - this.checkForComodification(); - - try { - ArrayList.this.set(this.lastRet, var1); - } catch (IndexOutOfBoundsException var3) { - throw new ConcurrentModificationException(); - } - } - } - - public void add(E var1) { - this.checkForComodification(); - - try { - int var2 = this.cursor; - ArrayList.this.add(var2, var1); - this.cursor = var2 + 1; - this.lastRet = -1; - this.expectedModCount = ArrayList.this.modCount; - } catch (IndexOutOfBoundsException var3) { - throw new ConcurrentModificationException(); - } - } - } - - private class Itr implements Iterator { - int cursor; - int lastRet = -1; - int expectedModCount; - - Itr() { - this.expectedModCount = ArrayList.this.modCount; - } - - public boolean hasNext() { - return this.cursor != ArrayList.this.size; - } - - public E next() { - this.checkForComodification(); - int var1 = this.cursor; - if (var1 >= ArrayList.this.size) { - throw new NoSuchElementException(); - } else { - Object[] var2 = ArrayList.this.elementData; - if (var1 >= var2.length) { - throw new ConcurrentModificationException(); - } else { - this.cursor = var1 + 1; - return var2[this.lastRet = var1]; - } - } - } - - public void remove() { - if (this.lastRet < 0) { - throw new IllegalStateException(); - } else { - this.checkForComodification(); - - try { - ArrayList.this.remove(this.lastRet); - this.cursor = this.lastRet; - this.lastRet = -1; - this.expectedModCount = ArrayList.this.modCount; - } catch (IndexOutOfBoundsException var2) { - throw new ConcurrentModificationException(); - } - } - } - - public void forEachRemaining(Consumer var1) { - Objects.requireNonNull(var1); - int var2 = ArrayList.this.size; - int var3 = this.cursor; - if (var3 < var2) { - Object[] var4 = ArrayList.this.elementData; - if (var3 >= var4.length) { - throw new ConcurrentModificationException(); - } - - while(var3 < var2 && ArrayList.this.modCount == this.expectedModCount) { - var1.accept(ArrayList.elementAt(var4, var3)); - ++var3; - } - - this.cursor = var3; - this.lastRet = var3 - 1; - this.checkForComodification(); - } - - } - - final void checkForComodification() { - if (ArrayList.this.modCount != this.expectedModCount) { - throw new ConcurrentModificationException(); - } - } - } - - private static class SubList extends AbstractList implements RandomAccess { - private final ArrayList root; - private final SubList parent; - private final int offset; - private int size; - - public SubList(ArrayList var1, int var2, int var3) { - this.root = var1; - this.parent = null; - this.offset = var2; - this.size = var3 - var2; - this.modCount = var1.modCount; - } - - private SubList(SubList var1, int var2, int var3) { - this.root = var1.root; - this.parent = var1; - this.offset = var1.offset + var2; - this.size = var3 - var2; - this.modCount = var1.modCount; - } - - public E set(int var1, E var2) { - Objects.checkIndex(var1, this.size); - this.checkForComodification(); - Object var3 = this.root.elementData(this.offset + var1); - this.root.elementData[this.offset + var1] = var2; - return var3; - } - - public E get(int var1) { - Objects.checkIndex(var1, this.size); - this.checkForComodification(); - return this.root.elementData(this.offset + var1); - } - - public int size() { - this.checkForComodification(); - return this.size; - } - - public void add(int var1, E var2) { - this.rangeCheckForAdd(var1); - this.checkForComodification(); - this.root.add(this.offset + var1, var2); - this.updateSizeAndModCount(1); - } - - public E remove(int var1) { - Objects.checkIndex(var1, this.size); - this.checkForComodification(); - Object var2 = this.root.remove(this.offset + var1); - this.updateSizeAndModCount(-1); - return var2; - } - - protected void removeRange(int var1, int var2) { - this.checkForComodification(); - this.root.removeRange(this.offset + var1, this.offset + var2); - this.updateSizeAndModCount(var1 - var2); - } - - public boolean addAll(Collection var1) { - return this.addAll(this.size, var1); - } - - public boolean addAll(int var1, Collection var2) { - this.rangeCheckForAdd(var1); - int var3 = var2.size(); - if (var3 == 0) { - return false; - } else { - this.checkForComodification(); - this.root.addAll(this.offset + var1, var2); - this.updateSizeAndModCount(var3); - return true; - } - } - - public void replaceAll(UnaryOperator var1) { - this.root.replaceAllRange(var1, this.offset, this.offset + this.size); - } - - public boolean removeAll(Collection var1) { - return this.batchRemove(var1, false); - } - - public boolean retainAll(Collection var1) { - return this.batchRemove(var1, true); - } - - private boolean batchRemove(Collection var1, boolean var2) { - this.checkForComodification(); - int var3 = this.root.size; - boolean var4 = this.root.batchRemove(var1, var2, this.offset, this.offset + this.size); - if (var4) { - this.updateSizeAndModCount(this.root.size - var3); - } - - return var4; - } - - public boolean removeIf(Predicate var1) { - this.checkForComodification(); - int var2 = this.root.size; - boolean var3 = this.root.removeIf(var1, this.offset, this.offset + this.size); - if (var3) { - this.updateSizeAndModCount(this.root.size - var2); - } - - return var3; - } - - public Object[] toArray() { - this.checkForComodification(); - return Arrays.copyOfRange(this.root.elementData, this.offset, this.offset + this.size); - } - - public T[] toArray(T[] var1) { - this.checkForComodification(); - if (var1.length < this.size) { - return Arrays.copyOfRange(this.root.elementData, this.offset, this.offset + this.size, var1.getClass()); - } else { - System.arraycopy(this.root.elementData, this.offset, var1, 0, this.size); - if (var1.length > this.size) { - var1[this.size] = null; - } - - return var1; - } - } - - public boolean equals(Object var1) { - if (var1 == this) { - return true; - } else if (!(var1 instanceof List)) { - return false; - } else { - boolean var2 = this.root.equalsRange((List)var1, this.offset, this.offset + this.size); - this.checkForComodification(); - return var2; - } - } - - public int hashCode() { - int var1 = this.root.hashCodeRange(this.offset, this.offset + this.size); - this.checkForComodification(); - return var1; - } - - public int indexOf(Object var1) { - int var2 = this.root.indexOfRange(var1, this.offset, this.offset + this.size); - this.checkForComodification(); - return var2 >= 0 ? var2 - this.offset : -1; - } - - public int lastIndexOf(Object var1) { - int var2 = this.root.lastIndexOfRange(var1, this.offset, this.offset + this.size); - this.checkForComodification(); - return var2 >= 0 ? var2 - this.offset : -1; - } - - public boolean contains(Object var1) { - return this.indexOf(var1) >= 0; - } - - public Iterator iterator() { - return this.listIterator(); - } - - public ListIterator listIterator(final int var1) { - this.checkForComodification(); - this.rangeCheckForAdd(var1); - return new ListIterator() { - int cursor = var1; - int lastRet = -1; - int expectedModCount; - - { - this.expectedModCount = SubList.this.modCount; - } - - public boolean hasNext() { - return this.cursor != SubList.this.size; - } - - public E next() { - this.checkForComodification(); - int var1x = this.cursor; - if (var1x >= SubList.this.size) { - throw new NoSuchElementException(); - } else { - Object[] var2 = SubList.this.root.elementData; - if (SubList.this.offset + var1x >= var2.length) { - throw new ConcurrentModificationException(); - } else { - this.cursor = var1x + 1; - return var2[SubList.this.offset + (this.lastRet = var1x)]; - } - } - } - - public boolean hasPrevious() { - return this.cursor != 0; - } - - public E previous() { - this.checkForComodification(); - int var1x = this.cursor - 1; - if (var1x < 0) { - throw new NoSuchElementException(); - } else { - Object[] var2 = SubList.this.root.elementData; - if (SubList.this.offset + var1x >= var2.length) { - throw new ConcurrentModificationException(); - } else { - this.cursor = var1x; - return var2[SubList.this.offset + (this.lastRet = var1x)]; - } - } - } - - public void forEachRemaining(Consumer var1x) { - Objects.requireNonNull(var1x); - int var2 = SubList.this.size; - int var3 = this.cursor; - if (var3 < var2) { - Object[] var4 = SubList.this.root.elementData; - if (SubList.this.offset + var3 >= var4.length) { - throw new ConcurrentModificationException(); - } - - while(var3 < var2 && SubList.this.root.modCount == this.expectedModCount) { - var1x.accept(ArrayList.elementAt(var4, SubList.this.offset + var3)); - ++var3; - } - - this.cursor = var3; - this.lastRet = var3 - 1; - this.checkForComodification(); - } - - } - - public int nextIndex() { - return this.cursor; - } - - public int previousIndex() { - return this.cursor - 1; - } - - public void remove() { - if (this.lastRet < 0) { - throw new IllegalStateException(); - } else { - this.checkForComodification(); - - try { - SubList.this.remove(this.lastRet); - this.cursor = this.lastRet; - this.lastRet = -1; - this.expectedModCount = SubList.this.modCount; - } catch (IndexOutOfBoundsException var2) { - throw new ConcurrentModificationException(); - } - } - } - - public void set(E var1x) { - if (this.lastRet < 0) { - throw new IllegalStateException(); - } else { - this.checkForComodification(); - - try { - SubList.this.root.set(SubList.this.offset + this.lastRet, var1x); - } catch (IndexOutOfBoundsException var3) { - throw new ConcurrentModificationException(); - } - } - } - - public void add(E var1x) { - this.checkForComodification(); - - try { - int var2 = this.cursor; - SubList.this.add(var2, var1x); - this.cursor = var2 + 1; - this.lastRet = -1; - this.expectedModCount = SubList.this.modCount; - } catch (IndexOutOfBoundsException var3) { - throw new ConcurrentModificationException(); - } - } - - final void checkForComodification() { - if (SubList.this.root.modCount != this.expectedModCount) { - throw new ConcurrentModificationException(); - } - } - }; - } - - public List subList(int var1, int var2) { - subListRangeCheck(var1, var2, this.size); - return new SubList(this, var1, var2); - } - - private void rangeCheckForAdd(int var1) { - if (var1 < 0 || var1 > this.size) { - throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1)); - } - } - - private String outOfBoundsMsg(int var1) { - return "Index: " + var1 + ", Size: " + this.size; - } - - private void checkForComodification() { - if (this.root.modCount != this.modCount) { - throw new ConcurrentModificationException(); - } - } - - private void updateSizeAndModCount(int var1) { - SubList var2 = this; - - do { - var2.size += var1; - var2.modCount = this.root.modCount; - var2 = var2.parent; - } while(var2 != null); - - } - fun `returns no highlights after cleared code`() { - val instance = Highlights.default() - - val highlights = instance.getHighlights() - public Spliterator spliterator() { - this.checkForComodification(); - return new Spliterator() { - private int index; - private int fence; - private int expectedModCount; - - assertTrue(highlights.isEmpty()) - { - this.index = SubList.this.offset; - this.fence = -1; - } - - instance.setCode("class ") - private int getFence() { - int var1; - if ((var1 = this.fence) < 0) { - this.expectedModCount = SubList.this.modCount; - var1 = this.fence = SubList.this.offset + SubList.this.size; - } - - val newHighlights = instance.getHighlights() - return var1; - } - - assertTrue(newHighlights.isNotEmpty()) - public ArrayList.ArrayListSpliterator trySplit() { - int var1 = this.getFence(); - int var2 = this.index; - int var3 = var2 + var1 >>> 1; - ArrayListSpliterator var10000; - if (var2 >= var3) { - var10000 = null; - } else { - ArrayList var10002 = SubList.this.root; - Objects.requireNonNull(var10002); - var10000 = var10002.new ArrayListSpliterator(var2, this.index = var3, this.expectedModCount); - } - - instance.setCode("") - return var10000; - } - - val emptyHighlights = instance.getHighlights() - public boolean tryAdvance(Consumer var1) { - Objects.requireNonNull(var1); - int var2 = this.getFence(); - int var3 = this.index; - if (var3 < var2) { - this.index = var3 + 1; - Object var4 = SubList.this.root.elementData[var3]; - var1.accept(var4); - if (SubList.this.root.modCount != this.expectedModCount) { - throw new ConcurrentModificationException(); - } else { - return true; - } - } else { - return false; - } - } - - assertTrue(emptyHighlights.isEmpty()) - } - public void forEachRemaining(Consumer var1) { - Objects.requireNonNull(var1); - ArrayList var5 = SubList.this.root; - Object[] var6; - if ((var6 = var5.elementData) != null) { - int var3; - int var4; - if ((var3 = this.fence) < 0) { - var4 = SubList.this.modCount; - var3 = SubList.this.offset + SubList.this.size; - } else { - var4 = this.expectedModCount; - } - - int var2; - if ((var2 = this.index) >= 0 && (this.index = var3) <= var6.length) { - while(var2 < var3) { - Object var7 = var6[var2]; - var1.accept(var7); - ++var2; - } - - if (var5.modCount == var4) { - return; - } - } - } - - throw new ConcurrentModificationException(); - } - - public long estimateSize() { - return (long)(this.getFence() - this.index); - } - - public int characteristics() { - return 16464; - } - }; - } - } - - final class ArrayListSpliterator implements Spliterator { - private int index; - private int fence; - private int expectedModCount; - - ArrayListSpliterator(int var2, int var3, int var4) { - this.index = var2; - this.fence = var3; - this.expectedModCount = var4; - } - - private int getFence() { - int var1; - if ((var1 = this.fence) < 0) { - this.expectedModCount = ArrayList.this.modCount; - var1 = this.fence = ArrayList.this.size; - } - - return var1; - } - - public ArrayList.ArrayListSpliterator trySplit() { - int var1 = this.getFence(); - int var2 = this.index; - int var3 = var2 + var1 >>> 1; - return var2 >= var3 ? null : ArrayList.this.new ArrayListSpliterator(var2, this.index = var3, this.expectedModCount); - } - - public boolean tryAdvance(Consumer var1) { - if (var1 == null) { - throw new NullPointerException(); - } else { - int var2 = this.getFence(); - int var3 = this.index; - if (var3 < var2) { - this.index = var3 + 1; - Object var4 = ArrayList.this.elementData[var3]; - var1.accept(var4); - if (ArrayList.this.modCount != this.expectedModCount) { - throw new ConcurrentModificationException(); - } else { - return true; - } - } else { - return false; - } - } - } - - public void forEachRemaining(Consumer var1) { - if (var1 == null) { - throw new NullPointerException(); - } else { - Object[] var5; - if ((var5 = ArrayList.this.elementData) != null) { - int var3; - int var4; - if ((var3 = this.fence) < 0) { - var4 = ArrayList.this.modCount; - var3 = ArrayList.this.size; - } else { - var4 = this.expectedModCount; - } - - int var2; - if ((var2 = this.index) >= 0 && (this.index = var3) <= var5.length) { - while(var2 < var3) { - Object var6 = var5[var2]; - var1.accept(var6); - ++var2; - } - - if (ArrayList.this.modCount == var4) { - return; - } - } - } - - throw new ConcurrentModificationException(); - } - } - - public long estimateSize() { - return (long)(this.getFence() - this.index); - } - - public int characteristics() { - return 16464; - } - } - } - """.trimIndent() } \ No newline at end of file From 0159c5ca42a8c1d7544f893a30e7c8a297420175 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Thu, 26 Sep 2024 08:11:23 +0200 Subject: [PATCH 10/11] Improved cancellation --- .../kotlin/dev/snipme/highlights/Highlights.kt | 11 +++++++++++ .../dev/snipme/highlights/HighlightsResultListener.kt | 4 ++-- .../dev/snipme/highlights/internal/HighlightsTest.kt | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 5eaf521..78b2439 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -15,7 +15,9 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException class Highlights private constructor( private var code: String, @@ -81,7 +83,9 @@ class Highlights private constructor( fun runJob() { CoroutineScope(Dispatchers.Default + errorHandler).launch { val structure = getCodeStructure() + analysisJob?.ensureActive() val highlights = constructHighlights(structure) + analysisJob?.ensureActive() listener.onComplete(highlights) }.also { analysisJob = it @@ -97,6 +101,9 @@ class Highlights private constructor( analysisJob?.cancel() } } catch (exception: Exception) { + if (exception is CancellationException) { + listener.onCancel() + } listener.onError(exception) } } @@ -111,6 +118,10 @@ class Highlights private constructor( fun getEmphasis() = emphasisLocations + fun clearSnapshot() { + snapshot = null + } + private fun constructHighlights(structure: CodeStructure): List = mutableListOf().apply { structure.marks.forEach { add(ColorHighlight(it, theme.mark)) } diff --git a/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt b/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt index 03cd1a9..d56b3b4 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt @@ -4,14 +4,14 @@ import dev.snipme.highlights.model.CodeHighlight interface HighlightsResultListener { fun onStart() - fun onComplete(highlights: List) + fun onComplete(result: List) fun onError(exception: Throwable) fun onCancel() } abstract class DefaultHighlightsResultListener : HighlightsResultListener { override fun onStart() {} - override fun onComplete(highlights: List) {} + override fun onComplete(result: List) {} override fun onError(exception: Throwable) {} override fun onCancel() {} } \ No newline at end of file diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index 065f24a..0d4c29f 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -177,6 +177,7 @@ class HighlightsTest { } } + highlights.clearSnapshot() invokeHighlightsRequest( highlights, onStart = { println("Start"); }, @@ -195,7 +196,7 @@ class HighlightsTest { ) { highlights.getHighlightsAsync(object : HighlightsResultListener { override fun onStart() = onStart() - override fun onComplete(highlights: List) = onCompleted(highlights) + override fun onComplete(result: List) = onCompleted(result) override fun onError(exception: Throwable) = onError(exception) override fun onCancel() = onCancel() }) From 3bb828e794caec76de343dd4c371b69b9f6528e3 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Fri, 1 Nov 2024 20:29:37 +0100 Subject: [PATCH 11/11] Removed job storing for async work --- .../dev/snipme/highlights/Highlights.kt | 44 +++---- .../highlights/HighlightsResultListener.kt | 4 +- .../highlights/internal/HighlightsTest.kt | 117 +++++++++++------- 3 files changed, 86 insertions(+), 79 deletions(-) diff --git a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt index 78b2439..bea335b 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt @@ -14,10 +14,9 @@ import dev.snipme.highlights.model.SyntaxThemes import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException class Highlights private constructor( private var code: String, @@ -25,10 +24,9 @@ class Highlights private constructor( private val theme: SyntaxTheme, private var emphasisLocations: List ) { - private var analysisJob: Job? = null + private var coroutineScope = CoroutineScope(Dispatchers.Default) - var snapshot: CodeSnapshot? = null - private set + private var snapshot: CodeSnapshot? = null companion object { fun default() = fromBuilder(Builder()) @@ -74,36 +72,22 @@ class Highlights private constructor( return constructHighlights(structure) } - fun getHighlightsAsync(listener: HighlightsResultListener) { + fun getHighlightsAsync(listener: HighlightsResultListener) = with(coroutineScope) { try { val errorHandler = CoroutineExceptionHandler { _, exception -> listener.onError(exception) } - fun runJob() { - CoroutineScope(Dispatchers.Default + errorHandler).launch { - val structure = getCodeStructure() - analysisJob?.ensureActive() - val highlights = constructHighlights(structure) - analysisJob?.ensureActive() - listener.onComplete(highlights) - }.also { - analysisJob = it - analysisJob!!.onCancel { listener.onCancel() } - listener.onStart() - } - } - - if (analysisJob == null || analysisJob?.isCompleted == true) { - runJob() - } else { - analysisJob!!.onCancel { runJob() } - analysisJob?.cancel() - } + coroutineContext.cancelChildren() + launch(errorHandler) { + listener.onStart() + ensureActive() + val structure = getCodeStructure() + ensureActive() + val highlights = constructHighlights(structure) + listener.onSuccess(highlights) + }.also { it.onCancel { listener.onCancel() } } } catch (exception: Exception) { - if (exception is CancellationException) { - listener.onCancel() - } listener.onError(exception) } } @@ -118,6 +102,8 @@ class Highlights private constructor( fun getEmphasis() = emphasisLocations + fun getSnapshot() = snapshot + fun clearSnapshot() { snapshot = null } diff --git a/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt b/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt index d56b3b4..61da299 100644 --- a/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt +++ b/src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt @@ -4,14 +4,14 @@ import dev.snipme.highlights.model.CodeHighlight interface HighlightsResultListener { fun onStart() - fun onComplete(result: List) + fun onSuccess(result: List) fun onError(exception: Throwable) fun onCancel() } abstract class DefaultHighlightsResultListener : HighlightsResultListener { override fun onStart() {} - override fun onComplete(result: List) {} + override fun onSuccess(result: List) {} override fun onError(exception: Throwable) {} override fun onCancel() {} } \ No newline at end of file diff --git a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt index 0d4c29f..7c0c0d6 100644 --- a/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt +++ b/src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt @@ -70,19 +70,22 @@ class HighlightsTest { setCode(longJavaCode) } - var wasCancelled = false + val results = mutableListOf>() + suspendCancellableCoroutine { continuation -> invokeHighlightsRequest( default, - onCancel = { wasCancelled = true }, + onSuccess = { results.add(it) }, onStart = { invokeHighlightsRequest(default) { - assertTrue { wasCancelled } + results.add(it) continuation.resume(Unit) {} } }, ) } + + assertTrue { results.size == 1 } } @Test @@ -129,8 +132,21 @@ class HighlightsTest { } println("Time2: ${time2.inWholeMilliseconds} ms") assertTrue { result2.isNotEmpty() } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +class HighlightsCancellationTest { + private val testDispatcher = StandardTestDispatcher() - assertTrue { time2.inWholeMilliseconds < time1.inWholeMilliseconds } + @BeforeTest + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() } @Test @@ -139,66 +155,71 @@ class HighlightsTest { setCode(longJavaCode) } - var time1: Duration - var time2: Duration + var time1 = Duration.ZERO + var time2 = Duration.ZERO - launch { + val job1 = launch { suspendCancellableCoroutine { c -> - invokeAndMeasureTime(default) { + invokeAndMeasureTime(default, "#1") { time1 = it c.resume(Unit) {} - println("Time1: ${time1.inWholeMilliseconds} ms") + println("Time1: ${it.inWholeMilliseconds} ms") } } } - launch { + val job2 = launch { suspendCancellableCoroutine { c -> - invokeAndMeasureTime(default) { + invokeAndMeasureTime(default, "#2") { time2 = it c.resume(Unit) {} - println("Time2: ${time2.inWholeMilliseconds} ms") + println("Time2: ${it.inWholeMilliseconds} ms") } } } - } - private fun invokeAndMeasureTime( - highlights: Highlights, - onFinish: (Duration) -> Unit = {} - ) { - var result: Duration? = null - val now = markNow() - - fun updateFirstTime() { - if (result == null) { - result = now.elapsedNow() - onFinish(result!!) - } + job1.join() + job2.join() + assertTrue { time1.inWholeMilliseconds < time2.inWholeMilliseconds } + } +} + +private fun invokeAndMeasureTime( + highlights: Highlights, + name: String, + onFinish: (Duration) -> Unit = {} +) { + var result: Duration? = null + val now = markNow() + + fun updateFirstTime() { + if (result == null) { + result = now.elapsedNow() + onFinish(result!!) } - - highlights.clearSnapshot() - invokeHighlightsRequest( - highlights, - onStart = { println("Start"); }, - onCancel = { println("Cancel"); updateFirstTime() }, - onError = { println("Error: $it"); updateFirstTime() }, - onCompleted = { println("Completed"); updateFirstTime() }, - ) } - private fun invokeHighlightsRequest( - highlights: Highlights, - onStart: () -> Unit = {}, - onCancel: () -> Unit = {}, - onError: (Throwable) -> Unit = {}, - onCompleted: (List) -> Unit = {}, - ) { - highlights.getHighlightsAsync(object : HighlightsResultListener { - override fun onStart() = onStart() - override fun onComplete(result: List) = onCompleted(result) - override fun onError(exception: Throwable) = onError(exception) - override fun onCancel() = onCancel() - }) - } + highlights.clearSnapshot() + invokeHighlightsRequest( + highlights, + onStart = { println("Start $name"); }, + onCancel = { println("Cancel $name"); updateFirstTime() }, + onError = { println("Error $name: $it"); updateFirstTime() }, + onSuccess = { println("Success $name"); updateFirstTime() }, + ) +} + +private fun invokeHighlightsRequest( + highlights: Highlights, + onStart: () -> Unit = {}, + onCancel: () -> Unit = {}, + onError: (Throwable) -> Unit = {}, + onSuccess: (List) -> Unit = {} +) { + highlights.getHighlightsAsync(object : HighlightsResultListener { + override fun onStart() = onStart() + override fun onSuccess(result: List) = onSuccess(result) + override fun onError(exception: Throwable) = onError(exception) + override fun onCancel() = onCancel() + }) } \ No newline at end of file