Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement lenses using codeVision package #2318

Merged
merged 8 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,117 +1,87 @@
package com.sourcegraph.cody.edit

import com.sourcegraph.cody.edit.actions.DocumentCodeAction
import com.sourcegraph.cody.edit.actions.lenses.EditAcceptAction
import com.sourcegraph.cody.edit.actions.lenses.EditCancelAction
import com.sourcegraph.cody.edit.actions.lenses.EditUndoAction
import com.sourcegraph.cody.edit.widget.LensAction
import com.sourcegraph.cody.edit.widget.LensHotkey
import com.sourcegraph.cody.edit.widget.LensIcon
import com.sourcegraph.cody.edit.widget.LensLabel
import com.sourcegraph.cody.edit.widget.LensSpinner
import com.sourcegraph.cody.edit.widget.LensWidgetGroup
import com.sourcegraph.cody.edit.lenses.actions.EditAcceptAction
import com.sourcegraph.cody.edit.lenses.actions.EditCancelAction
import com.sourcegraph.cody.edit.lenses.actions.EditUndoAction
import com.sourcegraph.cody.edit.lenses.providers.EditAcceptCodeVisionProvider
import com.sourcegraph.cody.edit.lenses.providers.EditCancelCodeVisionProvider
import com.sourcegraph.cody.edit.lenses.providers.EditUndoCodeVisionProvider
import com.sourcegraph.cody.edit.lenses.providers.EditWorkingCodeVisionProvider
import com.sourcegraph.cody.util.CodyIntegrationTextFixture
import com.sourcegraph.cody.util.CustomJunitClassRunner
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.startsWith
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(CustomJunitClassRunner::class)
class DocumentCodeTest : CodyIntegrationTextFixture() {

@Test
@Ignore
fun testGetsWorkingGroupLens() {
val codeLensGroup = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID)
val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID)

val inlayModel = myFixture.editor.inlayModel
val blockElements = inlayModel.getBlockElementsInRange(0, myFixture.editor.document.textLength)
val lensesGroups = blockElements.mapNotNull { it.renderer as? LensWidgetGroup }

assertEquals("There should be exactly one lenses group", 1, lensesGroups.size)

assertTrue("codeLensGroup cannot be null", codeLensGroup != null)
assertEquals("There are 2 lenses expected, working lens and cancel lens", 2, codeLenses.size)
// Lens group should match the expected structure.
val theWidgets = codeLensGroup!!.widgets
assertEquals(
"First lens should be working lens",
codeLenses[0].command?.command,
EditWorkingCodeVisionProvider.command)
assertEquals(
"Second lens should be cancel lens",
codeLenses[1].command?.command,
EditCancelCodeVisionProvider.command)

assertEquals("Lens group should have 9 widgets", 9, theWidgets.size)
assertTrue("Zeroth lens group should be an icon", theWidgets[0] is LensIcon)
assertTrue(
"First lens group is space separator label", (theWidgets[1] as LensLabel).text == " ")
assertTrue("Second lens group is a spinner", theWidgets[2] is LensSpinner)
assertTrue(
"Third lens group is space separator label", (theWidgets[3] as LensLabel).text == " ")
assertTrue(
"Fourth lens group is a description label",
(theWidgets[4] as LensAction).text == " Cody is working...")
assertTrue(
"Fifth lens group is separator label",
(theWidgets[5] as LensLabel).text == LensesService.SEPARATOR)
assertTrue("Sixth lens group should be an action", theWidgets[6] is LensAction)
assertTrue("Seventh lens group should be a label with a hotkey", theWidgets[7] is LensHotkey)
// We could try to Cancel the action, but there is no guarantee we can do it before edit will
// finish. It is safer to just wait for edit to finish and then undo it.
waitForSuccessfulEdit()

runLensAction(codeLensGroup, EditCancelAction.ID)
assertNoInlayShown()
runAndWaitForCleanState(EditUndoAction.ID)
}

@Test
fun testShowsAcceptLens() {
val codeLensGroup = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID)
assertInlayIsShown()
val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID)
assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty())

// Lens group should match the expected structure.
val inlayModel = myFixture.editor.inlayModel
val blockElements = inlayModel.getBlockElementsInRange(0, myFixture.editor.document.textLength)
val lensesGroups = blockElements.mapNotNull { it.renderer as? LensWidgetGroup }
val lenses = lensesGroups.firstOrNull()

assertNotNull("Lens group should be displayed", lenses)

val widgets = lenses!!.widgets
// There are 13 widgets as of the time of writing, but the UX could change, so check robustly.
assertTrue("Lens group should have at least 4 widgets", widgets.size >= 4)
assertNotNull(
"Lens group should contain Accept action",
widgets.find { widget -> widget is LensAction && widget.actionId == EditAcceptAction.ID })
assertNotNull(
"Lens group should contain Show Undo action",
widgets.find { widget -> widget is LensAction && widget.actionId == EditUndoAction.ID })
assertEquals("Lens group should have 4 lenses", 2, codeLenses.size)
assertEquals(
"First lens should be accept lens",
codeLenses[0].command?.command,
EditAcceptCodeVisionProvider.command)
assertEquals(
"Second lens should be undo lens",
codeLenses[1].command?.command,
EditUndoCodeVisionProvider.command)

// Make sure a doc comment was inserted.
assertTrue(hasJavadocComment(myFixture.editor.document.text))

runLensAction(codeLensGroup!!, EditUndoAction.ID)
assertNoInlayShown()
runAndWaitForCleanState(EditUndoAction.ID)
}

@Test
fun testAccept() {
assertNoInlayShown()
val acceptLens = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID)
assertTrue("Accept lens should be displayed", acceptLens != null)
assertInlayIsShown()
val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID)
assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty())

runLensAction(acceptLens!!, EditAcceptAction.ID)
assertNoInlayShown()
runAndWaitForCleanState(EditAcceptAction.ID)
assertThat(myFixture.editor.document.text, startsWith("/**"))
}

@Test
fun testUndo() {
val originalDocument = myFixture.editor.document.text
val undoLens = runAndWaitForLenses(DocumentCodeAction.ID, EditUndoAction.ID)
assertTrue("Undo lens should be displayed", undoLens != null)
val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditUndoAction.ID)
assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty())
assertNotSame(
"Expected document to be changed", originalDocument, myFixture.editor.document.text)
assertInlayIsShown()

runLensAction(undoLens!!, EditUndoAction.ID)
runAndWaitForCleanState(EditUndoAction.ID)
assertEquals(
"Expected document changes to be reverted",
originalDocument,
myFixture.editor.document.text)
assertNoInlayShown()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.EditorTestUtil
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.fixtures.BasePlatformTestCase
Expand All @@ -22,20 +23,17 @@ import com.sourcegraph.cody.agent.CodyAgentService
import com.sourcegraph.cody.agent.protocol_generated.ProtocolCodeLens
import com.sourcegraph.cody.config.CodyPersistentAccountsHost
import com.sourcegraph.cody.config.SourcegraphServerPath
import com.sourcegraph.cody.edit.LensListener
import com.sourcegraph.cody.edit.LensesService
import com.sourcegraph.cody.edit.widget.LensAction
import com.sourcegraph.cody.edit.widget.LensWidgetGroup
import com.sourcegraph.cody.edit.lenses.LensListener
import com.sourcegraph.cody.edit.lenses.LensesService
import com.sourcegraph.cody.edit.lenses.providers.EditAcceptCodeVisionProvider
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import junit.framework.TestCase

open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener {
private val logger = Logger.getInstance(CodyIntegrationTextFixture::class.java)
private val lensSubscribers =
mutableListOf<
Pair<(List<ProtocolCodeLens>) -> Boolean, CompletableFuture<LensWidgetGroup?>>>()
private val lensSubscribers = mutableListOf<(List<ProtocolCodeLens>) -> Boolean>()

override fun setUp() {
super.setUp()
Expand Down Expand Up @@ -196,82 +194,65 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener {
}
}

protected fun assertNoInlayShown() {
runInEdtAndWait {
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
assertFalse(
"Lens group inlay should NOT be displayed",
myFixture.editor.inlayModel.hasBlockElements())
}
override fun onLensesUpdate(vf: VirtualFile, codeLenses: List<ProtocolCodeLens>) {
synchronized(lensSubscribers) { lensSubscribers.removeAll { it(codeLenses) } }
}

protected fun assertInlayIsShown() {
runInEdtAndWait {
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
fun waitForSuccessfulEdit() {
var attempts = 0
val maxAttempts = 10

while (attempts < maxAttempts) {
val hasAcceptLens =
LensesService.getInstance(myFixture.project).getLenses(myFixture.editor).any {
it.command?.command == EditAcceptCodeVisionProvider.command
}

if (hasAcceptLens) break
Thread.sleep(1000)
attempts++
}
if (attempts >= maxAttempts) {
assertTrue(
"Lens group inlay should be displayed", myFixture.editor.inlayModel.hasBlockElements())
"Awaiting successful edit: No accept lens found after $maxAttempts attempts", false)
}
}

override fun onLensesUpdate(
lensWidgetGroup: LensWidgetGroup?,
codeLenses: List<ProtocolCodeLens>
) {
fun runAndWaitForCleanState(actionIdToRun: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need runAndWaitForCleanState? we could use runAndWaitForLenses directly

Copy link
Contributor Author

@pkukielka pkukielka Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but it is not obvious that runAndWaitForLenses(actionIdToRun) wait for no lenses to be visible anymore.
Or maybe I should change name of this function?

runAndWaitForLenses(actionIdToRun)
}

fun runAndWaitForLenses(
actionIdToRun: String,
vararg expectedLenses: String
): List<ProtocolCodeLens> {
val future = CompletableFuture<List<ProtocolCodeLens>>()
synchronized(lensSubscribers) {
lensSubscribers.removeAll { (checkFunc, future) ->
if (codeLenses.find { it.command?.command == "cody.fixup.codelens.error" } != null) {
future.completeExceptionally(IllegalStateException("Error group shown"))
return@removeAll true
lensSubscribers.add { codeLenses ->
val error = codeLenses.find { it.command?.command == "cody.fixup.codelens.error" }
if (error != null) {
future.completeExceptionally(
IllegalStateException("Error group shown: ${error.command?.title}"))
return@add false
}

val hasLensAppeared = checkFunc(codeLenses)
if (hasLensAppeared) future.complete(lensWidgetGroup)
hasLensAppeared
if ((expectedLenses.isEmpty() && codeLenses.isEmpty()) ||
expectedLenses.all { expected -> codeLenses.any { it.command?.command == expected } }) {
future.complete(codeLenses)
return@add true
}
return@add false
}
}
}

fun runAndWaitForLenses(actionId: String, actionLensId: String): LensWidgetGroup? {
val future = CompletableFuture<LensWidgetGroup?>()
val check = { codeLens: List<ProtocolCodeLens> ->
codeLens.any { it.command?.command == actionLensId }
}
lensSubscribers.add(check to future)

triggerAction(actionId)

try {
return future.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
} catch (e: Exception) {
val stackTrace = e.stackTrace.joinToString("\n") { it.toString() }
assertTrue(
"Error while awaiting condition after action $actionId: ${e.localizedMessage}\n$stackTrace",
false)
throw e
}
}

fun runLensAction(lensWidgetGroup: LensWidgetGroup, actionLensId: String): LensWidgetGroup? {
val future = CompletableFuture<LensWidgetGroup?>()
val check = { codeLens: List<ProtocolCodeLens> -> codeLens.isEmpty() }
lensSubscribers.add(check to future)

runInEdtAndWait {
val action: LensAction? =
lensWidgetGroup.widgets.filterIsInstance<LensAction>().find {
it.actionId == actionLensId
}
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
assertTrue("Lens action $actionLensId should be available", action != null)
action?.triggerAction(myFixture.editor)
}
triggerAction(actionIdToRun)

try {
return future.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
} catch (e: Exception) {
val stackTrace = e.stackTrace.joinToString("\n") { it.toString() }
val codeLenses = LensesService.getInstance(myFixture.project).getLenses(myFixture.editor)
assertTrue(
"Error while awaiting condition after lens action $actionLensId: ${e.localizedMessage}\n$stackTrace",
"Error while awaiting after action $actionIdToRun. Expected lenses: [${expectedLenses.joinToString()}], got: $codeLenses",
false)
throw e
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ data class TestingCredentials(
TestingCredentials(
token = System.getenv("SRC_DOTCOM_PRO_ACCESS_TOKEN"),
redactedToken =
"REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d",
"REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d",
serverEndpoint = ConfigUtil.DOTCOM_URL)
val dotcomProUserRateLimited =
TestingCredentials(
Expand Down
Loading
Loading