diff --git a/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt b/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt index f662511..8211d7e 100644 --- a/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt +++ b/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt @@ -417,6 +417,9 @@ class DocscribeAnnotator : ExternalAnnotator project: Project, projectDir: String, ) { + val now = System.currentTimeMillis() + if (!shouldShowGemBalloon(lastGemBalloonShown[projectDir], now)) return + lastGemBalloonShown[projectDir] = now try { val group = com.intellij.notification.NotificationGroupManager @@ -524,6 +527,31 @@ class DocscribeAnnotator : ExternalAnnotator companion object { private const val MAX_STDERR_PREVIEW = 200 + /** + * Minimum interval between "gem not installed" balloons per project directory. + * Prevents spamming the user on every opened file when the gem is missing. + */ + const val BALLOON_THROTTLE_MS = 15 * 60 * 1000L + + /** + * Last shown timestamp of the "gem not installed" balloon per project directory. + */ + @VisibleForTesting + internal val lastGemBalloonShown = ConcurrentHashMap() + + /** + * Whether the "gem not installed" balloon may be shown now. + * + * @param lastShownMs Previous show timestamp, or `null` if never shown. + * @param nowMs Current time in milliseconds. + * @return `true` on first show or when [BALLOON_THROTTLE_MS] elapsed. + */ + @JvmStatic + fun shouldShowGemBalloon( + lastShownMs: Long?, + nowMs: Long = System.currentTimeMillis(), + ): Boolean = lastShownMs == null || nowMs - lastShownMs >= BALLOON_THROTTLE_MS + /** * Generation counter per file path. * diff --git a/src/test/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotatorBalloonThrottleTest.kt b/src/test/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotatorBalloonThrottleTest.kt new file mode 100644 index 0000000..4f73dfd --- /dev/null +++ b/src/test/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotatorBalloonThrottleTest.kt @@ -0,0 +1,39 @@ +package com.florexlabs.docscribe.annotator + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class DocscribeAnnotatorBalloonThrottleTest { + @Test + fun `throttle interval is 15 minutes`() { + assertEquals(15 * 60 * 1000L, DocscribeAnnotator.BALLOON_THROTTLE_MS) + } + + @Test + fun `first show is allowed`() { + assertTrue(DocscribeAnnotator.shouldShowGemBalloon(null, 1_000L)) + } + + @Test + fun `repeat within window is suppressed`() { + val now = 1_000_000L + assertFalse(DocscribeAnnotator.shouldShowGemBalloon(now, now)) + assertFalse(DocscribeAnnotator.shouldShowGemBalloon(now - 1_000L, now)) + assertFalse(DocscribeAnnotator.shouldShowGemBalloon(now - DocscribeAnnotator.BALLOON_THROTTLE_MS + 1, now)) + } + + @Test + fun `show allowed exactly at boundary`() { + val now = 1_000_000L + assertTrue(DocscribeAnnotator.shouldShowGemBalloon(now - DocscribeAnnotator.BALLOON_THROTTLE_MS, now)) + } + + @Test + fun `show allowed after window elapsed`() { + val now = 1_000_000L + assertTrue(DocscribeAnnotator.shouldShowGemBalloon(now - DocscribeAnnotator.BALLOON_THROTTLE_MS - 1, now)) + assertTrue(DocscribeAnnotator.shouldShowGemBalloon(0L, now)) + } +}