From b451326eedd537f29a518befc02867bd72157969 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 22 Mar 2024 00:23:02 -0700 Subject: [PATCH 01/58] broken integration test attempt --- build.gradle.kts | 78 ++++++++++++++----- gradle.properties | 1 + .../sourcegraph/cody/edit/DocumentCodeTest.kt | 47 +++++++++++ 3 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6769c9fe9..8f34ebbbe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,19 +1,14 @@ + import com.jetbrains.plugin.structure.base.utils.isDirectory +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL -import java.nio.file.FileSystems -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.PathMatcher -import java.nio.file.Paths -import java.nio.file.SimpleFileVisitor -import java.nio.file.StandardCopyOption +import java.nio.file.* import java.nio.file.attribute.BasicFileAttributes -import java.util.EnumSet +import java.util.* import java.util.jar.JarFile import java.util.zip.ZipFile -import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String) = project.findProperty(key).toString() @@ -28,27 +23,41 @@ val isForceCodeSearchBuild = isForceBuild || properties("forceCodeSearchBuild") plugins { id("java") // Dependencies are locked at this version to work with JDK 11 on CI. - id("org.jetbrains.kotlin.jvm") version "1.9.22" + id("org.jetbrains.kotlin.jvm") version "1.9.22" // Also change in gradle.properties id("org.jetbrains.intellij") version "1.17.2" id("org.jetbrains.changelog") version "1.3.1" id("com.diffplug.spotless") version "6.25.0" } +val kotlinVersion: String by project +val platformVersion: String by project +val javaVersion: String by project + group = properties("pluginGroup") version = properties("pluginVersion") -repositories { mavenCentral() } - -intellij { - pluginName.set(properties("pluginName")) - version.set(properties("platformVersion")) - type.set(properties("platformType")) +repositories { + maven { url = uri("https://www.jetbrains.com/intellij-repository/releases") } + mavenCentral() +} - // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. - plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) +configurations { + create("intTestImplementation") { + extendsFrom(configurations.testImplementation.get()) + } + create("intTestRuntimeOnly") { + extendsFrom(configurations.testRuntimeOnly.get()) + } +} - updateSinceUntilBuild.set(false) +sourceSets { + val main by getting + val intTest by creating { + // Correctly access configurations for compileClasspath and runtimeClasspath + compileClasspath += main.output + configurations.named("intTestImplementation").get() + runtimeClasspath += output + compileClasspath + configurations.named("intTestRuntimeOnly").get() + } } dependencies { @@ -57,8 +66,23 @@ dependencies { implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.21.0") implementation("com.googlecode.java-diff-utils:diffutils:1.3.0") testImplementation("org.awaitility:awaitility-kotlin:4.2.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.7.0") + // testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.22") + + "intTestImplementation"("com.jetbrains.intellij.idea:ideaIC:2022.1") } +// Create a task to run integration tests +tasks.register("intTest") { + description = "Runs the integration tests." + group = "verification" + testClassesDirs = sourceSets["intTest"].output.classesDirs + classpath = sourceSets["intTest"].runtimeClasspath +} + +tasks.named("check") { dependsOn("intTest") } + spotless { java { target("src/*/java/**/*.java") @@ -74,9 +98,21 @@ spotless { ktfmt() trimTrailingWhitespace() target("src/**/*.kt") + toggleOffOn() } } +intellij { + pluginName.set(properties("pluginName")) + version.set(platformVersion) + type.set(properties("platformType")) + + // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. + plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) + + updateSinceUntilBuild.set(false) +} + java { toolchain { // Always compile the codebase with Java 11 regardless of what Java diff --git a/gradle.properties b/gradle.properties index 7eb712b92..9b5750841 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ platformPlugins=Git4Idea,PerforceDirectPlugin,java javaVersion=11 # Gradle Releases -> https://github.com/gradle/gradle/releases gradleVersion=8.1.1 +kotlinVersion=1.9.22 // Also change this in build.gradle.kts plugins section # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. # suppress inspection "UnusedProperty" diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt new file mode 100644 index 000000000..2caf1ab02 --- /dev/null +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -0,0 +1,47 @@ +package com.sourcegraph.cody.edit + +import com.intellij.testFramework.EditorTestUtil +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class DocumentCodeTest : BasePlatformTestCase() { + + override fun setUp() { + super.setUp() + // TODO: Project setup? + } + + fun testTopLevelFunction() { + // Steps: + // 1. Make a small kotlin file with a top level function + // - open it in the editor with CodeInsightTestFixture + // - use the markup language to specify the position + // 2. Invoke the "Document Code" action on the function + + // TODO: Move this into testData as a file with + // spotless:off + myFixture.configureByText("Foo.java", """ +import java.util.*; + +public class Foo { + + public void foo() { + List mystery = new ArrayList<>(); + mystery.add(0); + mystery.add(1); + for (int i = 2; i < 10; i++) { + mystery.add(mystery.get(i - 1) + mystery.get(i - 2)); + } + + for (int i = 0; i < 10; i++) { + System.out.println(mystery.get(i)); + } + } +} +""") // spotless:on + EditorTestUtil.executeAction(myFixture.editor, "cody.documentCodeAction") + } + + fun testTopLevelClass() { + // ... + } +} From b5d19c902aa6b81f63b32381ec14a5fe3c0599b0 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Fri, 22 Mar 2024 16:05:27 +0100 Subject: [PATCH 02/58] Make gradle integrationTests working --- build.gradle.kts | 73 ++++++++++--------- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 5 +- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8f34ebbbe..8e8471098 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,20 @@ - import com.jetbrains.plugin.structure.base.utils.isDirectory import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL -import java.nio.file.* +import java.nio.file.FileSystems +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.PathMatcher +import java.nio.file.Paths +import java.nio.file.SimpleFileVisitor +import java.nio.file.StandardCopyOption import java.nio.file.attribute.BasicFileAttributes -import java.util.* +import java.util.EnumSet import java.util.jar.JarFile import java.util.zip.ZipFile +import kotlin.script.experimental.jvm.util.hasParentNamed fun properties(key: String) = project.findProperty(key).toString() @@ -22,6 +28,7 @@ val isForceCodeSearchBuild = isForceBuild || properties("forceCodeSearchBuild") plugins { id("java") + id("jvm-test-suite") // Dependencies are locked at this version to work with JDK 11 on CI. id("org.jetbrains.kotlin.jvm") version "1.9.22" // Also change in gradle.properties id("org.jetbrains.intellij") version "1.17.2" @@ -42,24 +49,6 @@ repositories { mavenCentral() } -configurations { - create("intTestImplementation") { - extendsFrom(configurations.testImplementation.get()) - } - create("intTestRuntimeOnly") { - extendsFrom(configurations.testRuntimeOnly.get()) - } -} - -sourceSets { - val main by getting - val intTest by creating { - // Correctly access configurations for compileClasspath and runtimeClasspath - compileClasspath += main.output + configurations.named("intTestImplementation").get() - runtimeClasspath += output + compileClasspath + configurations.named("intTestRuntimeOnly").get() - } -} - dependencies { implementation("org.commonmark:commonmark:0.21.0") implementation("org.commonmark:commonmark-ext-gfm-tables:0.21.0") @@ -67,22 +56,9 @@ dependencies { implementation("com.googlecode.java-diff-utils:diffutils:1.3.0") testImplementation("org.awaitility:awaitility-kotlin:4.2.0") testImplementation("org.junit.jupiter:junit-jupiter:5.7.0") - // testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.22") - - "intTestImplementation"("com.jetbrains.intellij.idea:ideaIC:2022.1") } -// Create a task to run integration tests -tasks.register("intTest") { - description = "Runs the integration tests." - group = "verification" - testClassesDirs = sourceSets["intTest"].output.classesDirs - classpath = sourceSets["intTest"].runtimeClasspath -} - -tasks.named("check") { dependsOn("intTest") } - spotless { java { target("src/*/java/**/*.java") @@ -437,4 +413,33 @@ tasks { } test { dependsOn(project.tasks.getByPath("buildCody")) } + + configurations { + create("intTestImplementation") { extendsFrom(configurations.testImplementation.get()) } + create("intTestRuntimeClasspath") { extendsFrom(configurations.testRuntimeOnly.get()) } + } + + sourceSets { + create("intTest") { + kotlin.srcDir("src/integrationTest/kotlin") + compileClasspath += main.get().output + runtimeClasspath += main.get().output + } + } + + // Create a task to run integration tests + register("intTest") { + description = "Runs the integration tests." + group = "verification" + testClassesDirs = sourceSets["intTest"].output.classesDirs + classpath = sourceSets["intTest"].runtimeClasspath + + include { it.file.hasParentNamed("intTest") } + + useJUnit() + } + + named("intTest") { dependsOn("buildCody") } + + named("check") { dependsOn("intTest") } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 2caf1ab02..f847e798e 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -19,7 +19,9 @@ class DocumentCodeTest : BasePlatformTestCase() { // TODO: Move this into testData as a file with // spotless:off - myFixture.configureByText("Foo.java", """ + myFixture.configureByText( + "Foo.java", + """ import java.util.*; public class Foo { @@ -42,6 +44,7 @@ public class Foo { } fun testTopLevelClass() { + assert(false) // ... } } From ce239d69a2312d44741ff161f3fe39a8d0ca28c5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 24 Mar 2024 19:25:23 -0700 Subject: [PATCH 03/58] initial steps towards putting Agent in integration test mode --- build.gradle.kts | 8 ++++++-- .../com/sourcegraph/cody/edit/DocumentCodeTest.kt | 3 ++- .../com/sourcegraph/cody/agent/CodyAgentClient.java | 3 --- .../kotlin/com/sourcegraph/cody/agent/CodyAgent.kt | 12 ++++++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8e8471098..cad3f1b4a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ + import com.jetbrains.plugin.structure.base.utils.isDirectory import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel @@ -437,9 +438,12 @@ tasks { include { it.file.hasParentNamed("intTest") } useJUnit() - } - named("intTest") { dependsOn("buildCody") } + systemProperty("cody.integration.testing", "true") + environment("CODY_TESTING", "true") + + dependsOn("buildCody") + } named("check") { dependsOn("intTest") } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index f847e798e..f1cc38157 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -41,10 +41,11 @@ public class Foo { } """) // spotless:on EditorTestUtil.executeAction(myFixture.editor, "cody.documentCodeAction") + assertTrue(myFixture.editor.document.text.contains("/\\*")) } fun testTopLevelClass() { - assert(false) + assert(true) // ... } } diff --git a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java index 065108cf5..0750dfd8d 100644 --- a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java +++ b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java @@ -2,7 +2,6 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Editor; import com.sourcegraph.cody.agent.protocol.*; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -21,8 +20,6 @@ public class CodyAgentClient { private static final Logger logger = Logger.getInstance(CodyAgentClient.class); - @Nullable public Editor editor; - // Callback that is invoked when the agent sends a "chat/updateMessageInProgress" notification. @Nullable public Consumer onNewMessage; diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index b70670568..50ff24064 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -10,13 +10,13 @@ import com.intellij.util.system.CpuArch import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.cody.vscode.CancellationToken import com.sourcegraph.config.ConfigUtil +import org.eclipse.lsp4j.jsonrpc.Launcher import java.io.* import java.net.Socket import java.net.URI import java.nio.file.* import java.util.* import java.util.concurrent.* -import org.eclipse.lsp4j.jsonrpc.Launcher /** * Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for @@ -136,7 +136,8 @@ private constructor( val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") logger.info("using Cody agent script " + script.absolutePath) if (shouldSpawnDebuggableAgent()) { - listOf("node", "--inspect", "--enable-source-maps", script.absolutePath) + // TODO: Differentiate between --inspect and --inspect-brk (via env var) + listOf("node", "--inspect-brk", "--enable-source-maps", script.absolutePath) } else { listOf("node", "--enable-source-maps", script.absolutePath) } @@ -156,6 +157,13 @@ private constructor( processBuilder.environment()["CODY_LOG_EVENT_MODE"] = "connected-instance-only" } + // TODO: Figure out which of these two works best and remove the other one. + if (java.lang.Boolean.getBoolean("cody.integration.testing") + || System.getenv("CODY_TESTING") == "true") { + processBuilder.environment()["CODY_TESTING"] = "true" + processBuilder.environment()["CODY_SHIM_TESTING"] = "true" + } + val process = processBuilder .redirectErrorStream(false) From 4d84e12cfc891baa794fa7ff2f93715dca0bb8d9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 21 Mar 2024 16:54:39 -0700 Subject: [PATCH 04/58] Removed experimental user setting for inline edits We now have an environment-variable based feature flag, so the setting is no longer needed nor helpful. --- .../sourcegraph/cody/config/CodyApplicationSettings.kt | 2 -- .../com/sourcegraph/cody/config/SettingsModel.kt | 1 - .../com/sourcegraph/cody/config/ui/CodyConfigurable.kt | 10 ---------- .../sourcegraph/cody/edit/DocumentCodeActionHandler.kt | 4 +--- .../com/sourcegraph/cody/edit/EditCodeActionHandler.kt | 4 +--- src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt | 4 ---- 6 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyApplicationSettings.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyApplicationSettings.kt index b874cdf6c..d2a623ba5 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/CodyApplicationSettings.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyApplicationSettings.kt @@ -11,7 +11,6 @@ data class CodyApplicationSettings( var isCodyAutocompleteEnabled: Boolean = true, var isCodyDebugEnabled: Boolean = false, var isCodyVerboseDebugEnabled: Boolean = false, - var isCodyExperimentalInlineEditEnabled: Boolean = false, var isGetStartedNotificationDismissed: Boolean = false, var isNotLoggedInNotificationDismissed: Boolean = false, var anonymousUserId: String? = null, @@ -31,7 +30,6 @@ data class CodyApplicationSettings( this.isCodyAutocompleteEnabled = state.isCodyAutocompleteEnabled this.isCodyDebugEnabled = state.isCodyDebugEnabled this.isCodyVerboseDebugEnabled = state.isCodyVerboseDebugEnabled - this.isCodyExperimentalInlineEditEnabled = state.isCodyExperimentalInlineEditEnabled this.isGetStartedNotificationDismissed = state.isGetStartedNotificationDismissed this.isNotLoggedInNotificationDismissed = state.isNotLoggedInNotificationDismissed this.anonymousUserId = state.anonymousUserId diff --git a/src/main/kotlin/com/sourcegraph/cody/config/SettingsModel.kt b/src/main/kotlin/com/sourcegraph/cody/config/SettingsModel.kt index 87f39cf5f..f77f3019a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/SettingsModel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/SettingsModel.kt @@ -9,7 +9,6 @@ data class SettingsModel( var isCodyAutocompleteEnabled: Boolean = true, var isCodyDebugEnabled: Boolean = false, var isCodyVerboseDebugEnabled: Boolean = false, - var isCodyExperimentalInlineEditEnabled: Boolean = false, var isCustomAutocompleteColorEnabled: Boolean = false, var customAutocompleteColor: Color? = null, var isLookupAutocompleteEnabled: Boolean = true, diff --git a/src/main/kotlin/com/sourcegraph/cody/config/ui/CodyConfigurable.kt b/src/main/kotlin/com/sourcegraph/cody/config/ui/CodyConfigurable.kt index d4f63b31b..0a3bf06a3 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/ui/CodyConfigurable.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/ui/CodyConfigurable.kt @@ -48,12 +48,6 @@ class CodyConfigurable(val project: Project) : BoundConfigurable(ConfigUtil.CODY .enabledIf(enableCodyCheckbox.selected.and(enableDebugCheckbox.selected)) .bindSelected(settingsModel::isCodyVerboseDebugEnabled) } - row { - checkBox("Enable experimental inline edits") - .comment("Enables experimental edit-code and document-code features") - .enabledIf(enableCodyCheckbox.selected) - .bindSelected(settingsModel::isCodyExperimentalInlineEditEnabled) - } row { checkBox("Accept non-trusted certificates") .enabledIf(enableCodyCheckbox.selected) @@ -106,8 +100,6 @@ class CodyConfigurable(val project: Project) : BoundConfigurable(ConfigUtil.CODY settingsModel.isCodyAutocompleteEnabled = codyApplicationSettings.isCodyAutocompleteEnabled settingsModel.isCodyDebugEnabled = codyApplicationSettings.isCodyDebugEnabled settingsModel.isCodyVerboseDebugEnabled = codyApplicationSettings.isCodyVerboseDebugEnabled - settingsModel.isCodyExperimentalInlineEditEnabled = - codyApplicationSettings.isCodyExperimentalInlineEditEnabled settingsModel.isCustomAutocompleteColorEnabled = codyApplicationSettings.isCustomAutocompleteColorEnabled settingsModel.customAutocompleteColor = @@ -142,8 +134,6 @@ class CodyConfigurable(val project: Project) : BoundConfigurable(ConfigUtil.CODY codyApplicationSettings.isCodyAutocompleteEnabled = settingsModel.isCodyAutocompleteEnabled codyApplicationSettings.isCodyDebugEnabled = settingsModel.isCodyDebugEnabled codyApplicationSettings.isCodyVerboseDebugEnabled = settingsModel.isCodyVerboseDebugEnabled - codyApplicationSettings.isCodyExperimentalInlineEditEnabled = - settingsModel.isCodyExperimentalInlineEditEnabled codyApplicationSettings.isCustomAutocompleteColorEnabled = settingsModel.isCustomAutocompleteColorEnabled codyApplicationSettings.customAutocompleteColor = settingsModel.customAutocompleteColor?.rgb diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt index 91240bc77..a98adb0e8 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.editor.actionSystem.EditorAction import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.project.DumbAware import com.sourcegraph.cody.autocomplete.action.CodyAction -import com.sourcegraph.config.ConfigUtil import com.sourcegraph.utils.CodyEditorUtil class DocumentCodeAction : EditorAction(DocumentCodeActionHandler()), CodyAction, DumbAware @@ -17,8 +16,7 @@ class DocumentCodeActionHandler : EditorActionHandler() { private val logger = Logger.getInstance(DocumentCodeActionHandler::class.java) override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { - return CodyEditorUtil.isEditorValidForAutocomplete(editor) && - ConfigUtil.isExperimentalInlineEditEnabled() + return CodyEditorUtil.isEditorValidForAutocomplete(editor) } override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt index abbbeddd4..ce312b1d7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.editor.actionSystem.EditorAction import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.project.DumbAware import com.sourcegraph.cody.autocomplete.action.CodyAction -import com.sourcegraph.config.ConfigUtil import com.sourcegraph.utils.CodyEditorUtil class EditCodeAction : EditorAction(EditCodeActionHandler()), CodyAction, DumbAware @@ -17,8 +16,7 @@ class EditCodeActionHandler : EditorActionHandler() { private val logger = Logger.getInstance(EditCodeActionHandler::class.java) override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { - return CodyEditorUtil.isEditorValidForAutocomplete(editor) && - ConfigUtil.isExperimentalInlineEditEnabled() + return CodyEditorUtil.isEditorValidForAutocomplete(editor) } override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index 773d0fa7a..87f0ee662 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -129,10 +129,6 @@ object ConfigUtil { @JvmStatic fun isCodyDebugEnabled(): Boolean = CodyApplicationSettings.instance.isCodyDebugEnabled - @JvmStatic - fun isExperimentalInlineEditEnabled(): Boolean = - CodyApplicationSettings.instance.isCodyExperimentalInlineEditEnabled - @JvmStatic fun isCodyVerboseDebugEnabled(): Boolean = CodyApplicationSettings.instance.isCodyVerboseDebugEnabled From dacdd7649831e317441cbb5720917aa0caf5c8ef Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 27 Mar 2024 07:39:19 -0700 Subject: [PATCH 05/58] more work on integration tests --- build.gradle.kts | 19 +-- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 128 ++++++++++++++++-- .../java/com/sourcegraph/vcs/RepoUtil.java | 3 +- .../com/sourcegraph/cody/agent/CodyAgent.kt | 46 +++---- .../cody/agent/CodyAgentService.kt | 21 +-- .../autocomplete/CodyEditorFactoryListener.kt | 13 +- .../cody/edit/CodyInlineEditActionNotifier.kt | 35 +++++ .../com/sourcegraph/cody/edit/FixupService.kt | 2 +- .../com/sourcegraph/cody/edit/FixupSession.kt | 78 +++++++---- 9 files changed, 262 insertions(+), 83 deletions(-) create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt diff --git a/build.gradle.kts b/build.gradle.kts index cad3f1b4a..befbf497d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,21 +1,14 @@ - import com.jetbrains.plugin.structure.base.utils.isDirectory -import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL -import java.nio.file.FileSystems -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.PathMatcher -import java.nio.file.Paths -import java.nio.file.SimpleFileVisitor -import java.nio.file.StandardCopyOption +import java.nio.file.* import java.nio.file.attribute.BasicFileAttributes -import java.util.EnumSet +import java.util.* import java.util.jar.JarFile import java.util.zip.ZipFile import kotlin.script.experimental.jvm.util.hasParentNamed +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String) = project.findProperty(key).toString() @@ -99,6 +92,8 @@ java { } } +tasks.named("classpathIndexCleanup") { dependsOn("compileIntTestKotlin") } + fun download(url: String, output: File) { if (output.exists()) { println("Cached $output") diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index f1cc38157..729539bc6 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -2,22 +2,112 @@ package com.sourcegraph.cody.edit import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.messages.Topic +import com.sourcegraph.cody.edit.widget.LensAction +import com.sourcegraph.cody.edit.widget.LensLabel +import com.sourcegraph.cody.edit.widget.LensSpinner +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException class DocumentCodeTest : BasePlatformTestCase() { override fun setUp() { super.setUp() - // TODO: Project setup? + configureFixture() } - fun testTopLevelFunction() { - // Steps: - // 1. Make a small kotlin file with a top level function - // - open it in the editor with CodeInsightTestFixture - // - use the markup language to specify the position - // 2. Invoke the "Document Code" action on the function + fun testGetsWorkingGroupLens() { + val foldingRangeFuture = CompletableFuture() + project.messageBus + .connect() + .subscribe( + CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES, + object : CodyInlineEditActionNotifier { + override fun afterAction(context: CodyInlineEditActionNotifier.Context) { + testSelectionRange(context) + foldingRangeFuture.complete(null) + } + }) + // TODO: Solve the missing document problem. + // Hypothesis: We never send a file-opened notification for configuration above + // - normally we do it in + // - workAroundUninitializedCodeBase might be a place to set a breakpoint at + // Current problem is shown first in getFoldingRanges: + // in agent.ts, registered authenticated request 'editTask/getFoldingRanges' + // - File path passed in is "temp:///src/Foo.java" + // - uri parses correctly + // - this.workspace.getDocument(uri) is undefined + // + val editor = myFixture.editor + assertFalse(editor.inlayModel.hasBlockElements()) - // TODO: Move this into testData as a file with + // Execute the action and await the working group lens. + EditorTestUtil.executeAction(editor, "cody.documentCodeAction") + + val context = waitForTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) + assertNotNull("Timed out waiting for working group lens", context) + + // The inlay should be up. + assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) + + // Lens group should match the expected structure. + val lenses = context!!.session.lensGroup + assertNotNull("Lens group should be displayed", lenses) + + val widgets = lenses!!.widgets + assertEquals("Lens group should have 3 lenses", 3, widgets.size) + assertTrue("First lens should be a spinner", widgets[0] is LensSpinner) + assertTrue("Second lens should be a label", widgets[1] is LensLabel) + assertTrue("Third lens should be an action", widgets[2] is LensAction) + + // We make the editTask/getFoldingRanges call before calling commands/document. + // It's not supposed to be possible for them to be out of order, but this ensures + // that if they wind up out of order, both paths are tested. + try { + foldingRangeFuture.get(5, TimeUnit.SECONDS) + } catch (e: TimeoutException) { + fail("Folding range future did not complete within 5 seconds") + } catch (e: Exception) { + fail("Unexpected exception: ${e.message}") + } + } + + @RequiresEdt + private fun testSelectionRange(context: CodyInlineEditActionNotifier.Context) { + // TODO: Test/check selection range & fail test + // Ensure we were able to get the selection range. + val selection = context.session.selectionRange + assertNotNull("Selection should have been set", selection) + // We set the selection range to whatever the protocol returns. + // If a 0-width selection turns out to be reasonable we can adjust or remove this test. + assertFalse("Selection range should not be zero-width", selection!!.start == selection.end) + // A more robust check is to see if the selection "range" is just the caret position. + // If so, then our fallback range somehow made the round trip, which is bad. The lenses will go + // in the wrong places, etc. + val document = myFixture.editor.document + val startOffset = selection.start.toOffset(document) + val endOffset = selection.end.toOffset(document) + val caret = myFixture.editor.caretModel.primaryCaret.offset + assertFalse( + "Selection range should not equal the caret position", + startOffset == caret && endOffset == caret) + } + + // Next up: + // - test Cancel + // - test Accept + // - test workspace/edit + // - assertTrue(myFixture.editor.document.text.contains("/\\*")) + // - test Undo + + fun testTopLevelClass() { + assert(true) + // ... + } + + private fun configureFixture() { // spotless:off myFixture.configureByText( "Foo.java", @@ -40,12 +130,24 @@ public class Foo { } } """) // spotless:on - EditorTestUtil.executeAction(myFixture.editor, "cody.documentCodeAction") - assertTrue(myFixture.editor.document.text.contains("/\\*")) } - fun testTopLevelClass() { - assert(true) - // ... + // Block until the passed topic gets a message, or until we time out. + private fun waitForTopic( + topic: Topic + ): CodyInlineEditActionNotifier.Context? { + val future = + CompletableFuture() + .completeOnTimeout(null, 5, TimeUnit.SECONDS) + project.messageBus + .connect() + .subscribe( + topic, + object : CodyInlineEditActionNotifier { + override fun afterAction(context: CodyInlineEditActionNotifier.Context) { + future.complete(context) + } + }) + return future.get() } } diff --git a/src/main/java/com/sourcegraph/vcs/RepoUtil.java b/src/main/java/com/sourcegraph/vcs/RepoUtil.java index e9987c367..6b200a972 100644 --- a/src/main/java/com/sourcegraph/vcs/RepoUtil.java +++ b/src/main/java/com/sourcegraph/vcs/RepoUtil.java @@ -190,7 +190,8 @@ public static VCSType getVcsType(@NotNull Project project, @NotNull VirtualFile != null) { return VCSType.PERFORCE; } - } catch (ClassNotFoundException e) { + } catch (Exception e) { + // Can throw a ClassNotFoundException or a com.intellij.execution.ExecutionException/others // Perforce plugin is not installed. } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 50ff24064..de9df8d85 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -112,7 +112,7 @@ private constructor( ClientCapabilities( edit = "enabled", editWorkspace = "enabled", codeLenses = "enabled"))) .thenApply { info -> - logger.info("Connected to Cody agent " + info.name) + logger.warn("Connected to Cody agent " + info.name) server.initialized() CodyAgent(client, server, launcher, conn, listeningToJsonRpc) } @@ -132,24 +132,24 @@ private constructor( } val token = CancellationToken() val command: List = - if (System.getenv("CODY_DIR") != null) { - val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") - logger.info("using Cody agent script " + script.absolutePath) - if (shouldSpawnDebuggableAgent()) { - // TODO: Differentiate between --inspect and --inspect-brk (via env var) - listOf("node", "--inspect-brk", "--enable-source-maps", script.absolutePath) - } else { - listOf("node", "--enable-source-maps", script.absolutePath) - } - } else { - val binary = agentBinary(token) - logger.info("starting Cody agent " + binary.absolutePath) - listOf(binary.absolutePath) - } + if (System.getenv("CODY_DIR") != null) { + val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") + logger.info("using Cody agent script " + script.absolutePath) + if (shouldSpawnDebuggableAgent()) { + // TODO: Differentiate between --inspect and --inspect-brk (via env var) + listOf("node", "--inspect-brk", "--enable-source-maps", script.absolutePath) + } else { + listOf("node", "--enable-source-maps", script.absolutePath) + } + } else { + val binary = agentBinary(token) + logger.info("starting Cody agent " + binary.absolutePath) + listOf(binary.absolutePath) + } val processBuilder = ProcessBuilder(command) if (java.lang.Boolean.getBoolean("cody.accept-non-trusted-certificates-automatically") || - ConfigUtil.getShouldAcceptNonTrustedCertificatesAutomatically()) { + ConfigUtil.getShouldAcceptNonTrustedCertificatesAutomatically()) { processBuilder.environment()["NODE_TLS_REJECT_UNAUTHORIZED"] = "0" } @@ -158,17 +158,17 @@ private constructor( } // TODO: Figure out which of these two works best and remove the other one. - if (java.lang.Boolean.getBoolean("cody.integration.testing") - || System.getenv("CODY_TESTING") == "true") { + if (java.lang.Boolean.getBoolean("cody.integration.testing") || + System.getenv("CODY_TESTING") == "true") { processBuilder.environment()["CODY_TESTING"] = "true" processBuilder.environment()["CODY_SHIM_TESTING"] = "true" } val process = - processBuilder - .redirectErrorStream(false) - .redirectError(ProcessBuilder.Redirect.PIPE) - .start() + processBuilder + .redirectErrorStream(false) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() process.onExit().thenAccept { token.abort() } // Redirect agent stderr into idea.log by buffering line by line into `logger.warn()` @@ -177,7 +177,7 @@ private constructor( // agent shouldn't print much normally (excluding a few noisy messages during // initialization), it's mostly used to report unexpected errors. Thread { process.errorStream.bufferedReader().forEachLine { line -> logger.warn(line) } } - .start() + .start() return AgentConnection.ProcessConnection(process) } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 22b52095d..4be6395df 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -133,7 +133,8 @@ class CodyAgentService(project: Project) : Disposable { restartIfNeeded: Boolean, callback: Consumer, onFailure: Consumer = Consumer {} - ) { + ): CompletableFuture { + val future = CompletableFuture() if (CodyApplicationSettings.instance.isCodyEnabled) { ApplicationManager.getApplication().executeOnPooledThread { try { @@ -145,33 +146,37 @@ class CodyAgentService(project: Project) : Disposable { callback.accept(agent.get()) setAgentError(project, null) + future.complete(true) } catch (e: Exception) { logger.warn("Failed to execute call to agent", e) onFailure.accept(e) if (restartIfNeeded) getInstance(project).restartAgent(project) + future.completeExceptionally(e) // Complete the future exceptionally due to an error. } } + } else { + future.complete(false) // Complete the future with false indicating Cody is disabled. } + return future } @JvmStatic - fun withAgent( - project: Project, - callback: Consumer, - ) = withAgent(project, restartIfNeeded = false, callback = callback) + fun withAgent(project: Project, callback: Consumer): CompletableFuture = + withAgent(project, restartIfNeeded = false, callback = callback) @JvmStatic fun withAgentRestartIfNeeded( project: Project, - callback: Consumer, - ) = withAgent(project, restartIfNeeded = true, callback = callback) + callback: Consumer + ): CompletableFuture = withAgent(project, restartIfNeeded = true, callback = callback) @JvmStatic fun withAgentRestartIfNeeded( project: Project, callback: Consumer, onFailure: Consumer - ) = withAgent(project, restartIfNeeded = true, callback = callback, onFailure = onFailure) + ): CompletableFuture = + withAgent(project, restartIfNeeded = true, callback = callback, onFailure = onFailure) @JvmStatic fun isConnected(project: Project): Boolean { diff --git a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt index 57d4f34dc..a738647c2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt @@ -3,7 +3,14 @@ package com.sourcegraph.cody.autocomplete import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.event.* +import com.intellij.openapi.editor.event.BulkAwareDocumentListener +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager @@ -63,7 +70,9 @@ class CodyEditorFactoryListener : EditorFactoryListener { if (commandName == VIM_EXIT_INSERT_MODE_ACTION) { return } - Util.informAgentAboutEditorChange(e.editor) + // TODO: This is sending a redundant textDocument/didOpen to the Agent when first opening a + // file. + // Util.informAgentAboutEditorChange(e.editor) val suggestions = instance val editor = e.editor if (isEditorValidForAutocomplete(editor) && Util.isSelectedEditor(editor)) { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt new file mode 100644 index 000000000..9957decbc --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt @@ -0,0 +1,35 @@ +package com.sourcegraph.cody.edit + +import com.intellij.util.messages.Topic + +/** Pubsub interface shared by all inline edit notifications that accept a FixupSession. */ +interface CodyInlineEditActionNotifier { + + // Encapsulates the FixupSession and allows adding new fields without breaking subscribers. + data class Context(val session: FixupSession) + + fun afterAction(context: Context) + + companion object { + /** Sent once we have established the selection range, after fetching folding ranges. */ + @JvmStatic + @Topic.ProjectLevel + val TOPIC_FOLDING_RANGES = + Topic.create( + "Sourcegraph Cody: Received folding ranges", CodyInlineEditActionNotifier::class.java) + + /** Sent when the "Cody is working..." lens is displayed during an inline edit. */ + @JvmStatic + @Topic.ProjectLevel + val TOPIC_DISPLAY_WORKING_GROUP = + Topic.create( + "Sourcegraph Cody: Cody working lens shown", CodyInlineEditActionNotifier::class.java) + + /** Sent after a workspace/edit is applied. */ + @JvmStatic + @Topic.ProjectLevel + val TOPIC_WORKSPACE_EDIT = + Topic.create( + "Sourcegraph Cody: Inline Edit completed", CodyInlineEditActionNotifier::class.java) + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt index 2b200cc5f..4110fa673 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt @@ -69,7 +69,7 @@ class FixupService(val project: Project) : Disposable { // edit. val session: FixupSession? = if (pendingSessions.isNotEmpty()) { - pendingSessions.first() + pendingSessions.firstOrNull() // I still see empty collections here (race?) } else { // TODO: This is what I'd like to be able to do, but it requires a // protocol change: diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index caad4cf2b..9bffef01f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -5,7 +5,11 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.* +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.editor.RangeMarker +import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager @@ -14,11 +18,17 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.messages.Topic import com.sourcegraph.cody.agent.CodyAgent import com.sourcegraph.cody.agent.CodyAgentCodebase import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.CommandExecuteParams -import com.sourcegraph.cody.agent.protocol.* +import com.sourcegraph.cody.agent.protocol.CodyTaskState +import com.sourcegraph.cody.agent.protocol.EditTask +import com.sourcegraph.cody.agent.protocol.GetFoldingRangeParams +import com.sourcegraph.cody.agent.protocol.Position +import com.sourcegraph.cody.agent.protocol.Range +import com.sourcegraph.cody.agent.protocol.TextEdit import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensWidgetGroup import java.util.concurrent.CancellationException @@ -44,9 +54,10 @@ abstract class FixupSession( private var performedEdits = false - private var lensGroup: LensWidgetGroup? = null + var lensGroup: LensWidgetGroup? = null + private set - private var selectionRange: Range? = null + var selectionRange: Range? = null private var rangeMarkers: MutableSet = mutableSetOf() @@ -113,25 +124,26 @@ abstract class FixupSession( } private fun ensureSelectionRange(agent: CodyAgent, caret: Int) { - val url = getDocumentUrl() - if (url != null) { - val future = CompletableFuture() - agent.server.getFoldingRanges(GetFoldingRangeParams(uri = url)).handle { result, error -> - if (result != null && error == null) { - selectionRange = findRangeEnclosing(result.ranges, caret) - } - // Make sure we have SOME selection range near the caret. - // Otherwise, we wind up with the lenses and insertion at top of file. - if (selectionRange == null) { - logger.warn("Unable to find enclosing folding range at $caret in $url") - selectionRange = - Range(Position.fromOffset(document, caret), Position.fromOffset(document, caret)) - } - future.complete(null) + val url = getDocumentUrl() ?: return + val future = CompletableFuture() + agent.server.getFoldingRanges(GetFoldingRangeParams(uri = url)).handle { result, error -> + if (result != null && error == null) { + selectionRange = findRangeEnclosing(result.ranges, caret) + } + // Make sure we have SOME selection range near the caret. + // Otherwise, we wind up with the lenses and insertion at top of file. + if (selectionRange == null) { + logger.warn("Unable to find enclosing folding range at $caret in $url") + selectionRange = + Range(Position.fromOffset(document, caret), Position.fromOffset(document, caret)) + } else { + // This is useful for tracking issues with integration tests, but if it's annoying, ax it. + logger.warn("Found enclosing folding range at $caret in $url: $selectionRange") } - // Block until we get the folding ranges. - future.get() + future.complete(null) + publishProgressOnEdt(CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES) } + future.get() // Block until we have a selection range. } private fun getDocumentUrl(): String? { @@ -188,13 +200,16 @@ abstract class FixupSession( group.show(range) // Make sure the lens is visible. ApplicationManager.getApplication().invokeLater { - val logicalPosition = LogicalPosition(range.start.line, range.start.character) - editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) + if (!editor.isDisposed) { + val logicalPosition = LogicalPosition(range.start.line, range.start.character) + editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) + } } } private fun showWorkingGroup() { showLensGroup(LensGroupFactory(this).createTaskWorkingGroup()) + publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) } private fun showAcceptGroup() { @@ -267,6 +282,7 @@ abstract class FixupSession( else -> logger.warn("Unknown edit type: ${edit.type}") } } + publishProgress(CodyInlineEditActionNotifier.TOPIC_WORKSPACE_EDIT) } } } @@ -322,6 +338,22 @@ abstract class FixupSession( return FileEditorManager.getInstance(project).getEditors(file).firstOrNull() } + private fun publishProgress(topic: Topic) { + ApplicationManager.getApplication().executeOnPooledThread { + project.messageBus + .syncPublisher(topic) + .afterAction(CodyInlineEditActionNotifier.Context(session = this)) + } + } + + private fun publishProgressOnEdt(topic: Topic) { + ApplicationManager.getApplication().invokeLater { + project.messageBus + .syncPublisher(topic) + .afterAction(CodyInlineEditActionNotifier.Context(session = this)) + } + } + companion object { // Lens actions the user can take; we notify the Agent when they are taken. const val COMMAND_ACCEPT = "cody.fixup.codelens.accept" From ccfee5bc5196eee59dd36415f3f63a750bd399ef Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 29 Mar 2024 19:32:20 -0700 Subject: [PATCH 06/58] moved lens group visibility into the lens group --- .../com/sourcegraph/cody/edit/FixupSession.kt | 15 +++++---------- .../cody/edit/widget/LensWidgetGroup.kt | 14 +++++++++++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index 9bffef01f..a60ae1913 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -7,9 +7,7 @@ import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager @@ -17,6 +15,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic import com.sourcegraph.cody.agent.CodyAgent @@ -164,7 +163,7 @@ abstract class FixupSession( fun update(task: EditTask) { logger.warn("Task updated: $task") when (task.state) { - CodyTaskState.Idle -> {} + CodyTaskState.Idle -> {} // Internal state for pooled idle tasks. CodyTaskState.Working, CodyTaskState.Inserting, CodyTaskState.Applying, @@ -183,6 +182,7 @@ abstract class FixupSession( finish() } + @RequiresBackgroundThread private fun showLensGroup(group: LensWidgetGroup) { lensGroup?.let { if (!it.isDisposed.get()) Disposer.dispose(it) } lensGroup = group @@ -198,20 +198,15 @@ abstract class FixupSession( range = Range(start = position, end = position) } group.show(range) - // Make sure the lens is visible. - ApplicationManager.getApplication().invokeLater { - if (!editor.isDisposed) { - val logicalPosition = LogicalPosition(range.start.line, range.start.character) - editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) - } - } } + @RequiresBackgroundThread private fun showWorkingGroup() { showLensGroup(LensGroupFactory(this).createTaskWorkingGroup()) publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) } + @RequiresBackgroundThread private fun showAcceptGroup() { showLensGroup(LensGroupFactory(this).createAcceptGroup()) } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 66d81570d..1b45c76fe 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -6,6 +6,8 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorCustomElementRenderer import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.editor.event.EditorMouseEvent import com.intellij.openapi.editor.event.EditorMouseListener @@ -15,9 +17,14 @@ import com.intellij.openapi.editor.impl.FontInfo import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.Disposer import com.intellij.ui.Gray +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.FixupSession -import java.awt.* +import java.awt.Cursor +import java.awt.Font +import java.awt.FontMetrics +import java.awt.Graphics2D +import java.awt.Point import java.awt.geom.Rectangle2D import java.util.concurrent.atomic.AtomicBoolean @@ -95,6 +102,8 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : } } + // N.B. Blocks until the lens group is displayed, crossing thread boundaries. + @RequiresBackgroundThread fun show(range: Range) { commandCallbacks = session.commandCallbacks() val offset = range.start.toOffset(editor.document) @@ -102,6 +111,9 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : if (!isDisposed.get()) { inlay = editor.inlayModel.addBlockElement(offset, false, true, 0, this) Disposer.register(this, inlay!!) + // Make sure the lens is visible. + val logicalPosition = LogicalPosition(range.start.line, range.start.character) + editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) } } } From 932b1d27e7f39bb4ebc388ee07d4d299290e0507 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 29 Mar 2024 19:34:15 -0700 Subject: [PATCH 07/58] got unblocked on the EDT issue figured out how to run the tests on the JUnit runner thread --- build.gradle.kts | 12 ++- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 79 +++++++++---------- .../com/sourcegraph/cody/agent/CodyAgent.kt | 49 ++++++------ .../com/sourcegraph/cody/config/ServerAuth.kt | 9 +++ .../com/sourcegraph/config/ConfigUtil.kt | 7 ++ 5 files changed, 89 insertions(+), 67 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index befbf497d..448b20e53 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,12 @@ import com.jetbrains.plugin.structure.base.utils.isDirectory import java.net.URL -import java.nio.file.* +import java.nio.file.FileSystems +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.PathMatcher +import java.nio.file.Paths +import java.nio.file.SimpleFileVisitor +import java.nio.file.StandardCopyOption import java.nio.file.attribute.BasicFileAttributes import java.util.* import java.util.jar.JarFile @@ -435,6 +441,10 @@ tasks { useJUnit() systemProperty("cody.integration.testing", "true") + systemProperty( + "idea.test.execution.policy", // For now, should be used by all tests + "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") + environment("CODY_TESTING", "true") dependsOn("buildCody") diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 729539bc6..aa61fab93 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -2,6 +2,7 @@ package com.sourcegraph.cody.edit import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.runInEdtAndWait import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic import com.sourcegraph.cody.edit.widget.LensAction @@ -9,7 +10,6 @@ import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException class DocumentCodeTest : BasePlatformTestCase() { @@ -19,32 +19,13 @@ class DocumentCodeTest : BasePlatformTestCase() { } fun testGetsWorkingGroupLens() { - val foldingRangeFuture = CompletableFuture() - project.messageBus - .connect() - .subscribe( - CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES, - object : CodyInlineEditActionNotifier { - override fun afterAction(context: CodyInlineEditActionNotifier.Context) { - testSelectionRange(context) - foldingRangeFuture.complete(null) - } - }) - // TODO: Solve the missing document problem. - // Hypothesis: We never send a file-opened notification for configuration above - // - normally we do it in - // - workAroundUninitializedCodeBase might be a place to set a breakpoint at - // Current problem is shown first in getFoldingRanges: - // in agent.ts, registered authenticated request 'editTask/getFoldingRanges' - // - File path passed in is "temp:///src/Foo.java" - // - uri parses correctly - // - this.workspace.getDocument(uri) is undefined - // + val foldingRangeFuture = listenForFoldingRangeReply() + val editor = myFixture.editor assertFalse(editor.inlayModel.hasBlockElements()) // Execute the action and await the working group lens. - EditorTestUtil.executeAction(editor, "cody.documentCodeAction") + runInEdtAndWait { EditorTestUtil.executeAction(editor, "cody.documentCodeAction") } val context = waitForTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) assertNotNull("Timed out waiting for working group lens", context) @@ -52,26 +33,32 @@ class DocumentCodeTest : BasePlatformTestCase() { // The inlay should be up. assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) + foldingRangeFuture.get() // TODO: Do something here. + // Lens group should match the expected structure. val lenses = context!!.session.lensGroup assertNotNull("Lens group should be displayed", lenses) val widgets = lenses!!.widgets - assertEquals("Lens group should have 3 lenses", 3, widgets.size) + assertEquals("Lens group should have 6 widgets", 6, widgets.size) assertTrue("First lens should be a spinner", widgets[0] is LensSpinner) - assertTrue("Second lens should be a label", widgets[1] is LensLabel) - assertTrue("Third lens should be an action", widgets[2] is LensAction) - - // We make the editTask/getFoldingRanges call before calling commands/document. - // It's not supposed to be possible for them to be out of order, but this ensures - // that if they wind up out of order, both paths are tested. - try { - foldingRangeFuture.get(5, TimeUnit.SECONDS) - } catch (e: TimeoutException) { - fail("Folding range future did not complete within 5 seconds") - } catch (e: Exception) { - fail("Unexpected exception: ${e.message}") - } + assertTrue("Second lens should be a label", widgets[3] is LensLabel) + assertTrue("Third lens should be an action", widgets[5] is LensAction) + } + + private fun listenForFoldingRangeReply(): + CompletableFuture { + val foldingRangeFuture = CompletableFuture() + project.messageBus + .connect() + .subscribe( + CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES, + object : CodyInlineEditActionNotifier { + override fun afterAction(context: CodyInlineEditActionNotifier.Context) { + foldingRangeFuture.complete(context) + } + }) + return foldingRangeFuture } @RequiresEdt @@ -102,10 +89,10 @@ class DocumentCodeTest : BasePlatformTestCase() { // - assertTrue(myFixture.editor.document.text.contains("/\\*")) // - test Undo - fun testTopLevelClass() { - assert(true) - // ... - } + // fun testTopLevelClass() { + // assert(true) + // // ... + // } private fun configureFixture() { // spotless:off @@ -138,7 +125,7 @@ public class Foo { ): CodyInlineEditActionNotifier.Context? { val future = CompletableFuture() - .completeOnTimeout(null, 5, TimeUnit.SECONDS) + .completeOnTimeout(null, ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) project.messageBus .connect() .subscribe( @@ -150,4 +137,12 @@ public class Foo { }) return future.get() } + + companion object { + + // TODO: find the lowest value this can be for production, and use it + // If it's too low the test may be flaky. + + const val ASYNC_WAIT_TIMEOUT_SECONDS = 50L // 5L for non-debugging + } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index de9df8d85..d83565f9f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -10,13 +10,13 @@ import com.intellij.util.system.CpuArch import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.cody.vscode.CancellationToken import com.sourcegraph.config.ConfigUtil -import org.eclipse.lsp4j.jsonrpc.Launcher import java.io.* import java.net.Socket import java.net.URI import java.nio.file.* import java.util.* import java.util.concurrent.* +import org.eclipse.lsp4j.jsonrpc.Launcher /** * Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for @@ -132,24 +132,24 @@ private constructor( } val token = CancellationToken() val command: List = - if (System.getenv("CODY_DIR") != null) { - val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") - logger.info("using Cody agent script " + script.absolutePath) - if (shouldSpawnDebuggableAgent()) { - // TODO: Differentiate between --inspect and --inspect-brk (via env var) - listOf("node", "--inspect-brk", "--enable-source-maps", script.absolutePath) - } else { - listOf("node", "--enable-source-maps", script.absolutePath) - } - } else { - val binary = agentBinary(token) - logger.info("starting Cody agent " + binary.absolutePath) - listOf(binary.absolutePath) - } + if (System.getenv("CODY_DIR") != null) { + val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") + logger.info("using Cody agent script " + script.absolutePath) + if (shouldSpawnDebuggableAgent()) { + // TODO: Differentiate between --inspect and --inspect-brk (via env var) + listOf("node", "--inspect-brk", "--enable-source-maps", script.absolutePath) + } else { + listOf("node", "--enable-source-maps", script.absolutePath) + } + } else { + val binary = agentBinary(token) + logger.info("starting Cody agent " + binary.absolutePath) + listOf(binary.absolutePath) + } val processBuilder = ProcessBuilder(command) if (java.lang.Boolean.getBoolean("cody.accept-non-trusted-certificates-automatically") || - ConfigUtil.getShouldAcceptNonTrustedCertificatesAutomatically()) { + ConfigUtil.getShouldAcceptNonTrustedCertificatesAutomatically()) { processBuilder.environment()["NODE_TLS_REJECT_UNAUTHORIZED"] = "0" } @@ -157,18 +157,19 @@ private constructor( processBuilder.environment()["CODY_LOG_EVENT_MODE"] = "connected-instance-only" } - // TODO: Figure out which of these two works best and remove the other one. - if (java.lang.Boolean.getBoolean("cody.integration.testing") || - System.getenv("CODY_TESTING") == "true") { + if (ConfigUtil.isIntegrationTestModeEnabled()) { processBuilder.environment()["CODY_TESTING"] = "true" processBuilder.environment()["CODY_SHIM_TESTING"] = "true" + processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = + System.getenv("CODY_INTEGRATION_TEST_TOKEN") + ?: throw Error("No access token set for integration tests") } val process = - processBuilder - .redirectErrorStream(false) - .redirectError(ProcessBuilder.Redirect.PIPE) - .start() + processBuilder + .redirectErrorStream(false) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() process.onExit().thenAccept { token.abort() } // Redirect agent stderr into idea.log by buffering line by line into `logger.warn()` @@ -177,7 +178,7 @@ private constructor( // agent shouldn't print much normally (excluding a few noisy messages during // initialization), it's mostly used to report unexpected errors. Thread { process.errorStream.bufferedReader().forEachLine { line -> logger.warn(line) } } - .start() + .start() return AgentConnection.ProcessConnection(process) } diff --git a/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt b/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt index edf43c6b0..efb78de32 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt @@ -20,6 +20,15 @@ object ServerAuthLoader { return ServerAuth( defaultAccount.server.url, accessToken, defaultAccount.server.customRequestHeaders) } + if (ConfigUtil.isIntegrationTestModeEnabled()) { + val token = System.getenv("CODY_INTEGRATION_TEST_TOKEN") + if (token != null) { + return ServerAuth(ConfigUtil.DOTCOM_URL, token, "") + } else { + throw IllegalArgumentException( + "Integration testing enabled but no CODY_INTEGRATION_TEST_TOKEN passed") + } + } return ServerAuth(ConfigUtil.DOTCOM_URL, "", "") } } diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index 87f0ee662..ca446c7b5 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -177,4 +177,11 @@ object ConfigUtil { fun getShouldAcceptNonTrustedCertificatesAutomatically(): Boolean { return CodyApplicationSettings.instance.shouldAcceptNonTrustedCertificatesAutomatically } + + @JvmStatic + fun isIntegrationTestModeEnabled(): Boolean { + // TODO: Figure out which one to use and stick with it. + return java.lang.Boolean.getBoolean("cody.integration.testing") || + System.getenv("CODY_TESTING") == "true" + } } From 7dc4b37340c7e5344a800c336f990fb4031709f0 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 30 Mar 2024 02:14:30 -0700 Subject: [PATCH 08/58] got first integration tests working LLM interaction is not simulated yet, but coming soon --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 20 ++++++++++++--- .../com/sourcegraph/cody/edit/FixupService.kt | 4 +-- .../com/sourcegraph/cody/edit/FixupSession.kt | 17 ++++++++++--- .../sourcegraph/cody/edit/widget/LensLabel.kt | 3 ++- .../cody/edit/widget/LensWidgetGroup.kt | 25 +++++++++++++------ .../test/NonEdtIdeaTestExecutionPolicy.kt | 21 ++++++++++++++++ 6 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/sourcegraph/cody/test/NonEdtIdeaTestExecutionPolicy.kt diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index aa61fab93..5542588a0 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -6,6 +6,7 @@ import com.intellij.testFramework.runInEdtAndWait import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic import com.sourcegraph.cody.edit.widget.LensAction +import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import java.util.concurrent.CompletableFuture @@ -33,7 +34,8 @@ class DocumentCodeTest : BasePlatformTestCase() { // The inlay should be up. assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) - foldingRangeFuture.get() // TODO: Do something here. + // This is done now. + runInEdtAndWait { testSelectionRange(foldingRangeFuture.get()) } // Lens group should match the expected structure. val lenses = context!!.session.lensGroup @@ -41,9 +43,19 @@ class DocumentCodeTest : BasePlatformTestCase() { val widgets = lenses!!.widgets assertEquals("Lens group should have 6 widgets", 6, widgets.size) - assertTrue("First lens should be a spinner", widgets[0] is LensSpinner) - assertTrue("Second lens should be a label", widgets[3] is LensLabel) - assertTrue("Third lens should be an action", widgets[5] is LensAction) + assertTrue("Zeroth lens should be a spinner", widgets[0] is LensSpinner) + assertTrue("First lens is space separator label", (widgets[1] as LensLabel).text == " ") + assertTrue("Second lens is working label", (widgets[2] as LensLabel).text.contains("working")) + assertTrue( + "Third lens is separator label", + (widgets[3] as LensLabel).text == LensGroupFactory.SEPARATOR) + assertTrue("Fourth lens should be an action", widgets[4] is LensAction) + assertTrue( + "Fifth lens should be a label with a hotkey", + (widgets[5] as LensLabel).text.matches(Regex(" \\(.+\\)"))) + + // TODO: The LensSpinner is not shut down (maybe not disposed) + } private fun listenForFoldingRangeReply(): diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt index 4110fa673..679d6e066 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt @@ -33,7 +33,7 @@ class FixupService(val project: Project) : Disposable { agent.client.setOnEditTaskDidUpdate { task -> val session = activeSessions[task.id] if (session == null) { - logger.warn("No session found for task ${task.id}") + logger.warn("onEditTaskDidUpdate: No session found for task ${task.id}") } else { session.update(task) } @@ -42,7 +42,7 @@ class FixupService(val project: Project) : Disposable { agent.client.setOnEditTaskDidDelete { task -> val session = activeSessions[task.id] if (session == null) { - logger.warn("No session found for task ${task.id}") + logger.warn("onEditTaskDidDelete: No session found for task ${task.id}") } else { session.taskDeleted() } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index a60ae1913..d654c0150 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -94,7 +94,12 @@ abstract class FixupSession( fixupService.removeSession(this) } else { taskId = result.id - selectionRange = result.selectionRange + // Sometimes even though we get a folding range back, we don't get a selection range. + if (result.selectionRange.start.isZero() && result.selectionRange.end.isZero()) { + logger.warn("Empty selection range returned by Agent: $result") + } else { + selectionRange = result.selectionRange + } fixupService.addSession(this) } null @@ -163,7 +168,10 @@ abstract class FixupSession( fun update(task: EditTask) { logger.warn("Task updated: $task") when (task.state) { - CodyTaskState.Idle -> {} // Internal state for pooled idle tasks. + // This is an internal state (parked/ready tasks) and we should never see it. + CodyTaskState.Idle -> {} + // These four may or may not all arrive, depending on the operation, testing, etc. + // They are all sent in quick succession and any one can substitute for another. CodyTaskState.Working, CodyTaskState.Inserting, CodyTaskState.Applying, @@ -182,6 +190,9 @@ abstract class FixupSession( finish() } + // N.B. Blocks calling thread until the lens group is shown, + // which may require switching to the EDT. This is primarily to help smooth + // integration testing, but also because there's no real harm blocking pool threads. @RequiresBackgroundThread private fun showLensGroup(group: LensWidgetGroup) { lensGroup?.let { if (!it.isDisposed.get()) Disposer.dispose(it) } @@ -197,7 +208,7 @@ abstract class FixupSession( val position = Position(range.start.line, 0) range = Range(start = position, end = position) } - group.show(range) + group.show(range).get() } @RequiresBackgroundThread diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt index 20b1801b6..0d1bd6f4d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt @@ -1,9 +1,10 @@ package com.sourcegraph.cody.edit.widget +import org.jetbrains.annotations.VisibleForTesting import java.awt.FontMetrics import java.awt.Graphics2D -class LensLabel(group: LensWidgetGroup, private val text: String) : LensWidget(group) { +class LensLabel(group: LensWidgetGroup, @VisibleForTesting val text: String) : LensWidget(group) { override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = fontMetrics.stringWidth(text) override fun calcHeightInPixels(fontMetrics: FontMetrics): Int = fontMetrics.height diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 1b45c76fe..360e0b3c2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -26,6 +26,7 @@ import java.awt.FontMetrics import java.awt.Graphics2D import java.awt.Point import java.awt.geom.Rectangle2D +import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean operator fun Point.component1() = this.x @@ -102,20 +103,28 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : } } - // N.B. Blocks until the lens group is displayed, crossing thread boundaries. @RequiresBackgroundThread - fun show(range: Range) { + fun show(range: Range): CompletableFuture { commandCallbacks = session.commandCallbacks() val offset = range.start.toOffset(editor.document) + val future = CompletableFuture() ApplicationManager.getApplication().invokeLater { - if (!isDisposed.get()) { - inlay = editor.inlayModel.addBlockElement(offset, false, true, 0, this) - Disposer.register(this, inlay!!) - // Make sure the lens is visible. - val logicalPosition = LogicalPosition(range.start.line, range.start.character) - editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) + try { + if (isDisposed.get()) { + future.complete(false) + } else { + inlay = editor.inlayModel.addBlockElement(offset, false, true, 0, this) + Disposer.register(this, inlay!!) + // Make sure the lens is visible. + val logicalPosition = LogicalPosition(range.start.line, range.start.character) + editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) + future.complete(true) + } + } catch (ex: Throwable) { + future.completeExceptionally(ex) } } + return future } // Propagate repaint requests from widgets to the inlay. diff --git a/src/main/kotlin/com/sourcegraph/cody/test/NonEdtIdeaTestExecutionPolicy.kt b/src/main/kotlin/com/sourcegraph/cody/test/NonEdtIdeaTestExecutionPolicy.kt new file mode 100644 index 000000000..639efdbe8 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/test/NonEdtIdeaTestExecutionPolicy.kt @@ -0,0 +1,21 @@ +package com.sourcegraph.cody.test + +import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy + +/** + * Used for all Cody JetBrains integration tests. You have to specify it via the System property + * `idea.test.execution.policy` in order to run the tests. + */ +@Suppress("unused") +class NonEdtIdeaTestExecutionPolicy : IdeaTestExecutionPolicy() { + + override fun getName(): String = javaClass.name + + /** + * This setting enables our integration tests. If they use the default policy and run on the EDT, + * then they either deadlock or finish prematurely, because they cannot block on our long-running + * multithreaded async backend operations. With this set to false, we run on the JUnit runner + * thread, which can block. + */ + override fun runInDispatchThread() = false +} From cfac44da38c2d8f70aa58fe96e9485f016af18a3 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 30 Mar 2024 12:56:03 -0700 Subject: [PATCH 09/58] fixed broken integration test --- .../kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt | 7 +++++-- src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt | 3 --- .../kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 5542588a0..917e50a72 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -1,5 +1,6 @@ package com.sourcegraph.cody.edit +import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.runInEdtAndWait @@ -54,8 +55,10 @@ class DocumentCodeTest : BasePlatformTestCase() { "Fifth lens should be a label with a hotkey", (widgets[5] as LensLabel).text.matches(Regex(" \\(.+\\)"))) - // TODO: The LensSpinner is not shut down (maybe not disposed) - + // This avoids an error saying the spinner hasn't shut down, at the end of the test. + runInEdtAndWait { + Disposer.dispose(lenses) + } } private fun listenForFoldingRangeReply(): diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index d654c0150..1ff68a23d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -140,9 +140,6 @@ abstract class FixupSession( logger.warn("Unable to find enclosing folding range at $caret in $url") selectionRange = Range(Position.fromOffset(document, caret), Position.fromOffset(document, caret)) - } else { - // This is useful for tracking issues with integration tests, but if it's annoying, ax it. - logger.warn("Found enclosing folding range at $caret in $url: $selectionRange") } future.complete(null) publishProgressOnEdt(CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt index 7c8471bf1..64d510b4e 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt @@ -52,7 +52,11 @@ class LensSpinner(group: LensWidgetGroup, private val icon: Icon) : LensWidget(g } override fun dispose() { - stop() + try { + stop() + } catch (x: Exception) { + logger.error(x) + } } override fun toString(): String { From bac793b3293c95ca2523b1c2ce4fa23779432570 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 30 Mar 2024 13:29:38 -0700 Subject: [PATCH 10/58] got integration tests working from cli/gradle --- CONTRIBUTING.md | 32 +++++++++++++++++++ .../com/sourcegraph/cody/agent/CodyAgent.kt | 13 +++++--- .../com/sourcegraph/cody/config/ServerAuth.kt | 5 +-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d459b33f..77d5c85c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -201,6 +201,19 @@ to use if you are uncertain which method to use for debugging. Option 2 is especially useful when you need to set a breakpoint very early in the Agent startup. +### Using VS Code to debug the Agent + +The setup described here also works with VS Code as your debugger and/or +launcher. Both configurations work with IntelliJ and VS Code on the Cody +side, and the VS Code Cody extension has both configurations available +in its default run configurations. + +VS Code is usually the better choice for a debugger, because the JetBrains +TypeScript plugin gets very confused over our TypeScript code base, and +cannot search for files or symbols. + +Sometimes using JetBrains is more convenient, so we describe both options. + ## How to set up Run Configurations Run configurations are basically IDEA's launcher scripts. You will need @@ -381,3 +394,22 @@ see the configuration dropdown at the top. - Workaround is to exit the target gracefully by quitting each time, using the menus or hotkeys, rather than force-stopping it. +# Integration Testing + +Run the integration tests at the command line with: + +``` +./gradlew intTest +``` + +If you pass in an access token, it will use the default production LLM +rather than a mock LLM, which can be useful when updating the test if +the protocol changes. + +``` +CODY_INTEGRATION_TEST_TOKEN=sgp_asdfasdfasdfasdfasdfasdfasdf +``` + +You can run and debug the integration tests, including the Agent node +process, with the instructions above by making new run configurations +for the test. diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index d83565f9f..4eec38fb2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -10,13 +10,13 @@ import com.intellij.util.system.CpuArch import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.cody.vscode.CancellationToken import com.sourcegraph.config.ConfigUtil +import org.eclipse.lsp4j.jsonrpc.Launcher import java.io.* import java.net.Socket import java.net.URI import java.nio.file.* import java.util.* import java.util.concurrent.* -import org.eclipse.lsp4j.jsonrpc.Launcher /** * Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for @@ -160,9 +160,14 @@ private constructor( if (ConfigUtil.isIntegrationTestModeEnabled()) { processBuilder.environment()["CODY_TESTING"] = "true" processBuilder.environment()["CODY_SHIM_TESTING"] = "true" - processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = - System.getenv("CODY_INTEGRATION_TEST_TOKEN") - ?: throw Error("No access token set for integration tests") + val testToken = System.getenv("CODY_INTEGRATION_TEST_TOKEN") + // The Cody side will use the real LLM if this token is present, + // so you can run the integration tests against a prod LLM rather than a mock. + if (testToken is String && testToken.isNotBlank()) { + processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = testToken + } else { + logger.warn("No access token passed for integration tests; using mock LLM") + } } val process = diff --git a/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt b/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt index efb78de32..37b086d39 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/ServerAuth.kt @@ -1,5 +1,6 @@ package com.sourcegraph.cody.config +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.sourcegraph.config.ConfigUtil @@ -10,6 +11,7 @@ data class ServerAuth( ) object ServerAuthLoader { + val logger = Logger.getInstance(ServerAuth::class.java) @JvmStatic fun loadServerAuth(project: Project): ServerAuth { @@ -25,8 +27,7 @@ object ServerAuthLoader { if (token != null) { return ServerAuth(ConfigUtil.DOTCOM_URL, token, "") } else { - throw IllegalArgumentException( - "Integration testing enabled but no CODY_INTEGRATION_TEST_TOKEN passed") + logger.warn("Integration testing enabled but no CODY_INTEGRATION_TEST_TOKEN passed") } } return ServerAuth(ConfigUtil.DOTCOM_URL, "", "") From ded891e18da39c0678bbe8ad015a0e713921633c Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 30 Mar 2024 19:45:01 -0700 Subject: [PATCH 11/58] more work on integration test framework --- .../cody/initialization/PostStartupActivity.kt | 17 +++++++++++++---- .../test-projects/document-code/.gitignore | 1 + .../document-code/src/main/java/Foo.java | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/test/resources/test-projects/document-code/.gitignore create mode 100644 src/test/resources/test-projects/document-code/src/main/java/Foo.java diff --git a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt index 111bd9377..2ea125639 100644 --- a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt +++ b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.Constraints import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.startup.StartupActivity import com.sourcegraph.cody.CodyFocusChangeListener @@ -23,6 +24,8 @@ import com.sourcegraph.telemetry.TelemetryInitializerActivity * compatibility. */ class PostStartupActivity : StartupActivity.DumbAware { + private val logger = Logger.getInstance(PostStartupActivity::class.java) + override fun runActivity(project: Project) { TelemetryInitializerActivity().runActivity(project) SettingsMigration().runActivity(project) @@ -40,10 +43,15 @@ class PostStartupActivity : StartupActivity.DumbAware { private fun initializeInlineEdits() { ApplicationManager.getApplication().invokeLater { if (ConfigUtil.isFeatureFlagEnabled("cody.feature.inline-edits")) { - val actionManager = ActionManager.getInstance() - (actionManager.getAction("CodyEditorActions") as? DefaultActionGroup)?.apply { - pushFrontAction(actionManager, "cody.documentCodeAction", this) - pushFrontAction(actionManager, "cody.editCodeAction", this) + try { + val actionManager = ActionManager.getInstance() + (actionManager.getAction("CodyEditorActions") as? DefaultActionGroup)?.apply { + pushFrontAction(actionManager, "cody.documentCodeAction", this) + pushFrontAction(actionManager, "cody.editCodeAction", this) + } + } catch (x: Exception) { + // We should still start up gracefully, so just log a warning. + logger.warn("Failed to initialize inline edits", x) } } } @@ -54,6 +62,7 @@ class PostStartupActivity : StartupActivity.DumbAware { actionId: String, group: DefaultActionGroup ) { + // If the group already contains the action, do nothing. actionManager.getAction(actionId)?.let { group.add(it, Constraints.FIRST) } } } diff --git a/src/test/resources/test-projects/document-code/.gitignore b/src/test/resources/test-projects/document-code/.gitignore new file mode 100644 index 000000000..1cc4572e1 --- /dev/null +++ b/src/test/resources/test-projects/document-code/.gitignore @@ -0,0 +1 @@ +.idea/workspace.xml diff --git a/src/test/resources/test-projects/document-code/src/main/java/Foo.java b/src/test/resources/test-projects/document-code/src/main/java/Foo.java new file mode 100644 index 000000000..dbcf323ca --- /dev/null +++ b/src/test/resources/test-projects/document-code/src/main/java/Foo.java @@ -0,0 +1,17 @@ +import java.util.*; + +public class Foo { + + public void foo() { + List mystery = new ArrayList<>(); + mystery.add(0); + mystery.add(1); + for (int i = 2; i < 10; i++) { + mystery.add(mystery.get(i - 1) + mystery.get(i - 2)); + } + + for (int i = 0; i < 10; i++) { + System.out.println(mystery.get(i)); + } + } +} From 4edd3a17526fe9213afc24e393930330d1988a73 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 30 Mar 2024 23:46:32 -0700 Subject: [PATCH 12/58] fixed a deadlock in the tests --- .../com/sourcegraph/cody/edit/FixupSession.kt | 6 +++-- .../cody/edit/widget/LensWidgetGroup.kt | 26 ++++++++++++++++--- .../document-code/src/main/java/Foo.java | 7 +++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index 1ff68a23d..44521a2e1 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -190,7 +190,6 @@ abstract class FixupSession( // N.B. Blocks calling thread until the lens group is shown, // which may require switching to the EDT. This is primarily to help smooth // integration testing, but also because there's no real harm blocking pool threads. - @RequiresBackgroundThread private fun showLensGroup(group: LensWidgetGroup) { lensGroup?.let { if (!it.isDisposed.get()) Disposer.dispose(it) } lensGroup = group @@ -205,7 +204,10 @@ abstract class FixupSession( val position = Position(range.start.line, 0) range = Range(start = position, end = position) } - group.show(range).get() + val future = group.show(range) + if (!ApplicationManager.getApplication().isDispatchThread) { + future.get() + } } @RequiresBackgroundThread diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 360e0b3c2..d556eb146 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -17,9 +17,9 @@ import com.intellij.openapi.editor.impl.FontInfo import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.Disposer import com.intellij.ui.Gray -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.FixupSession +import org.jetbrains.annotations.NotNull import java.awt.Cursor import java.awt.Font import java.awt.FontMetrics @@ -28,6 +28,7 @@ import java.awt.Point import java.awt.geom.Rectangle2D import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Supplier operator fun Point.component1() = this.x @@ -103,12 +104,11 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : } } - @RequiresBackgroundThread fun show(range: Range): CompletableFuture { commandCallbacks = session.commandCallbacks() val offset = range.start.toOffset(editor.document) val future = CompletableFuture() - ApplicationManager.getApplication().invokeLater { + onEventThread { try { if (isDisposed.get()) { future.complete(false) @@ -257,6 +257,26 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : } } + private fun onEventThread(handler: Supplier): @NotNull CompletableFuture { + val result = CompletableFuture() + if (ApplicationManager.getApplication().isDispatchThread) { + try { + result.complete(null) + } catch (e: Exception) { + result.completeExceptionally(e) + } + } else { + ApplicationManager.getApplication().invokeLater { + try { + result.complete(handler.get()) + } catch (e: Exception) { + result.completeExceptionally(e) + } + } + } + return result + } + companion object { private val lensColor = Gray._150 } diff --git a/src/test/resources/test-projects/document-code/src/main/java/Foo.java b/src/test/resources/test-projects/document-code/src/main/java/Foo.java index dbcf323ca..36ffc8ce2 100644 --- a/src/test/resources/test-projects/document-code/src/main/java/Foo.java +++ b/src/test/resources/test-projects/document-code/src/main/java/Foo.java @@ -1,3 +1,10 @@ +/** + * Calculates the sum of two integers. + * + * @param a the first integer to add + * @param b the second integer to add + * @return the sum of the two integers + */ import java.util.*; public class Foo { From 5bc49ba23f48545bf3c2e0fe9f13d03eb6ad6227 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 31 Mar 2024 14:19:42 -0700 Subject: [PATCH 13/58] removed an accidental comment --- .../test-projects/document-code/src/main/java/Foo.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/test/resources/test-projects/document-code/src/main/java/Foo.java b/src/test/resources/test-projects/document-code/src/main/java/Foo.java index 36ffc8ce2..dbcf323ca 100644 --- a/src/test/resources/test-projects/document-code/src/main/java/Foo.java +++ b/src/test/resources/test-projects/document-code/src/main/java/Foo.java @@ -1,10 +1,3 @@ -/** - * Calculates the sum of two integers. - * - * @param a the first integer to add - * @param b the second integer to add - * @return the sum of the two integers - */ import java.util.*; public class Foo { From 189dd046fc6c23ae2f8604b6eb9d1715d01c89ac Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 31 Mar 2024 14:20:17 -0700 Subject: [PATCH 14/58] fixed showing the Accept lens group It had disappeared while re-threading things for integration tests --- .../com/sourcegraph/cody/edit/FixupSession.kt | 2 +- .../cody/edit/widget/LensWidgetGroup.kt | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index 44521a2e1..7b4f8a59d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -205,7 +205,7 @@ abstract class FixupSession( range = Range(start = position, end = position) } val future = group.show(range) - if (!ApplicationManager.getApplication().isDispatchThread) { + if (!ApplicationManager.getApplication().isDispatchThread) { // integration test future.get() } } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index d556eb146..ec47aa562 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -259,19 +259,19 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : private fun onEventThread(handler: Supplier): @NotNull CompletableFuture { val result = CompletableFuture() - if (ApplicationManager.getApplication().isDispatchThread) { + val executeAndComplete: () -> Unit = { try { - result.complete(null) + val value = handler.get() + result.complete(value) } catch (e: Exception) { result.completeExceptionally(e) } + } + if (ApplicationManager.getApplication().isDispatchThread) { + executeAndComplete() } else { ApplicationManager.getApplication().invokeLater { - try { - result.complete(handler.get()) - } catch (e: Exception) { - result.completeExceptionally(e) - } + executeAndComplete() } } return result From fdff59f1be2b0bbe53128a84d56dde55eaa04013 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 4 Apr 2024 09:00:35 -0700 Subject: [PATCH 15/58] added a TODO --- .../kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 917e50a72..835617c2b 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -20,6 +20,11 @@ class DocumentCodeTest : BasePlatformTestCase() { configureFixture() } + override fun tearDown() { + // TODO: Notify the Agent that all documents were closed. + super.tearDown() + } + fun testGetsWorkingGroupLens() { val foldingRangeFuture = listenForFoldingRangeReply() From 5efd0c5bf10c7b1d37af5dc7097973536e96d8ea Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 4 Apr 2024 21:15:50 -0700 Subject: [PATCH 16/58] merged from main and got tests (almost) working again required killing off the old agent process, which fixed some startup errors. More work to do --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 8 +-- .../cody/agent/CodyAgentClient.java | 24 ++------ .../com/sourcegraph/cody/agent/CodyAgent.kt | 55 +++++++++++++++++-- .../cody/agent/CodyAgentService.kt | 25 ++++----- .../com/sourcegraph/cody/edit/FixupService.kt | 7 ++- .../sourcegraph/cody/edit/widget/LensLabel.kt | 2 +- .../cody/edit/widget/LensWidgetGroup.kt | 6 +- .../initialization/PostStartupActivity.kt | 12 +++- 8 files changed, 85 insertions(+), 54 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 835617c2b..31e2cc05b 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -21,8 +21,8 @@ class DocumentCodeTest : BasePlatformTestCase() { } override fun tearDown() { - // TODO: Notify the Agent that all documents were closed. - super.tearDown() + // TODO: Notify the Agent that all documents were closed. + super.tearDown() } fun testGetsWorkingGroupLens() { @@ -61,9 +61,7 @@ class DocumentCodeTest : BasePlatformTestCase() { (widgets[5] as LensLabel).text.matches(Regex(" \\(.+\\)"))) // This avoids an error saying the spinner hasn't shut down, at the end of the test. - runInEdtAndWait { - Disposer.dispose(lenses) - } + runInEdtAndWait { Disposer.dispose(lenses) } } private fun listenForFoldingRangeReply(): diff --git a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java index adbc070b0..747200d72 100644 --- a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java +++ b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java @@ -33,24 +33,16 @@ public class CodyAgentClient { @Nullable public Consumer onReceivedWebviewMessage; // Callback for the "editTask/didUpdate" notification from the agent. - @Nullable private Consumer onEditTaskDidUpdate; + @Nullable public Consumer onEditTaskDidUpdate; // Callback for the "editTask/didDelete" notification from the agent. - @Nullable private Consumer onEditTaskDidDelete; + @Nullable public Consumer onEditTaskDidDelete; // Callback for the "textDocument/edit" request from the agent. - @Nullable private Consumer onTextDocumentEdit; + @Nullable public Consumer onTextDocumentEdit; // Callback for the "workspace/edit" request from the agent. - @Nullable private Consumer onWorkspaceEdit; - - public void setOnEditTaskDidUpdate(@Nullable Consumer callback) { - onEditTaskDidUpdate = callback; - } - - public void setOnEditTaskDidDelete(@Nullable Consumer callback) { - onEditTaskDidDelete = callback; - } + @Nullable public Consumer onWorkspaceEdit; @JsonNotification("editTask/didUpdate") public CompletableFuture editTaskDidUpdate(EditTask params) { @@ -62,19 +54,11 @@ public CompletableFuture editTaskDidDelete(EditTask params) { return acceptOnEventThread("editTask/didDelete", onEditTaskDidDelete, params); } - public void setOnTextDocumentEdit(@Nullable Consumer callback) { - onTextDocumentEdit = callback; - } - @JsonRequest("textDocument/edit") public CompletableFuture textDocumentEdit(TextDocumentEditParams params) { return acceptOnEventThread("textDocument/edit", onTextDocumentEdit, params); } - public void setOnWorkspaceEdit(@Nullable Consumer callback) { - onWorkspaceEdit = callback; - } - @JsonRequest("workspace/edit") public CompletableFuture workspaceEdit(WorkspaceEditParams params) { return acceptOnEventThread("workspace/edit", onWorkspaceEdit, params); diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 4eec38fb2..4a941bc6a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -10,13 +10,13 @@ import com.intellij.util.system.CpuArch import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.cody.vscode.CancellationToken import com.sourcegraph.config.ConfigUtil -import org.eclipse.lsp4j.jsonrpc.Launcher import java.io.* import java.net.Socket import java.net.URI import java.nio.file.* import java.util.* import java.util.concurrent.* +import org.eclipse.lsp4j.jsonrpc.Launcher /** * Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for @@ -127,6 +127,7 @@ private constructor( } private fun startAgentProcess(): AgentConnection { + killAnyCompetingDebugAgentProcess() if (ConfigUtil.shouldConnectToDebugAgent()) { return connectToDebugAgent() } @@ -136,7 +137,6 @@ private constructor( val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") logger.info("using Cody agent script " + script.absolutePath) if (shouldSpawnDebuggableAgent()) { - // TODO: Differentiate between --inspect and --inspect-brk (via env var) listOf("node", "--inspect-brk", "--enable-source-maps", script.absolutePath) } else { listOf("node", "--enable-source-maps", script.absolutePath) @@ -164,9 +164,9 @@ private constructor( // The Cody side will use the real LLM if this token is present, // so you can run the integration tests against a prod LLM rather than a mock. if (testToken is String && testToken.isNotBlank()) { - processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = testToken + processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = testToken } else { - logger.warn("No access token passed for integration tests; using mock LLM") + logger.warn("No access token passed for integration tests; using mock LLM") } } @@ -290,5 +290,52 @@ private constructor( val port = System.getenv("CODY_AGENT_DEBUG_PORT")?.toInt() ?: DEFAULT_AGENT_DEBUG_PORT return AgentConnection.SocketConnection(Socket("localhost", port)) } + + // Default port used when you pass --inspect or --inspect-brk to a node.js process. + private const val NODE_INSPECT_DEFAULT_PORT = 9229 + + private fun killAnyCompetingDebugAgentProcess() { + // We only kill an Agent process if it's on our port; i.e., a leftover zombie. + if (shouldSpawnDebuggableAgent()) { + try { + if (SystemInfoRt.isWindows) { + killAgentOnWindows() + } else { + killAgentOnUnix() + } + } catch (e: Exception) { + logger.warn("failed trying to kill existing agent process", e) + } + } + } + + private fun killAgentOnWindows() { + val port = NODE_INSPECT_DEFAULT_PORT + val findProcessCommand = "netstat -aon | findstr :$port" + val processBuilder = + ProcessBuilder("cmd.exe", "/c", findProcessCommand).redirectErrorStream(true) + val process = processBuilder.start() + val processOutput = process.inputStream.bufferedReader().readText().trim() + val pid = processOutput.substringAfterLast(" ").trim() + if (pid.isNotEmpty()) { + logger.warn("Killing process pid=$pid on port $port") + Runtime.getRuntime().exec("taskkill /F /PID $pid") + } + } + + private fun killAgentOnUnix() { + val port = NODE_INSPECT_DEFAULT_PORT + val pid = + Runtime.getRuntime() + .exec("lsof -ti tcp:$port") + .inputStream + .bufferedReader() + .readText() + .trim() + if (pid.isNotEmpty()) { + logger.warn("Killing process on port $port with PID $pid") + Runtime.getRuntime().exec("kill $pid") + } + } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 717f40c05..0de29a700 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -43,28 +43,23 @@ class CodyAgentService(project: Project) : Disposable { } } - agent.client.setOnEditTaskDidUpdate { task -> + agent.client.onEditTaskDidUpdate = Consumer { task -> FixupService.getInstance(project).getSessionForTask(task)?.update(task) } - agent.client.setOnEditTaskDidDelete { task -> - FixupService.getInstance(project).getSessionForTask(task)?.taskDeleted() - } - - agent.client.setOnWorkspaceEdit { params -> - // TODO: We should change the protocol and send `taskId` as part of `WorkspaceEditParam` - // and then use method like `getSessionForTask` instead of this one - FixupService.getInstance(project).getActiveSession()?.performWorkspaceEdit(params) - } - - agent.client.setOnTextDocumentEdit { params -> - // TODO: This one is missing + agent.client.onTextDocumentEdit = Consumer { params -> + // I think we're only using workspace/edit now -- namespace change? + logger.warn("textDocument/edit not supported in JetBrains") } if (!project.isDisposed) { AgentChatSessionService.getInstance(project).restoreAllSessions(agent) - FileEditorManager.getInstance(project).openFiles.forEach { file -> - CodyFileEditorListener.fileOpened(project, agent, file) + try { + FileEditorManager.getInstance(project).openFiles.forEach { file -> + CodyFileEditorListener.fileOpened(project, agent, file) + } + } catch (x: Exception) { + logger.warn("Error notifying Agent of open files", x) } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt index c6d6fdfb2..4d91dcac7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt @@ -10,6 +10,7 @@ import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.protocol.EditTask import com.sourcegraph.config.ConfigUtil.isCodyEnabled import com.sourcegraph.utils.CodyEditorUtil +import java.util.function.Consumer /** Controller for commands that allow the LLM to edit the code directly. */ @Service(Service.Level.PROJECT) @@ -29,7 +30,7 @@ class FixupService(val project: Project) : Disposable { init { // JetBrains docs say avoid heavy lifting in the constructor, so pass to another thread. CodyAgentService.withAgent(project) { agent -> - agent.client.setOnEditTaskDidUpdate { task -> + agent.client.onEditTaskDidUpdate = Consumer { task -> val session = activeSessions[task.id] if (session == null) { logger.warn("onEditTaskDidUpdate: No session found for task ${task.id}") @@ -38,7 +39,7 @@ class FixupService(val project: Project) : Disposable { } } - agent.client.setOnEditTaskDidDelete { task -> + agent.client.onEditTaskDidDelete = Consumer { task -> val session = activeSessions[task.id] if (session == null) { logger.warn("onEditTaskDidDelete: No session found for task ${task.id}") @@ -47,7 +48,7 @@ class FixupService(val project: Project) : Disposable { } } - agent.client.setOnWorkspaceEdit { params -> + agent.client.onWorkspaceEdit = Consumer { params -> for (op in params.operations) { // TODO: We need to support the file-level operations. when (op.type) { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt index 0d1bd6f4d..d4b8aa1e2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt @@ -1,8 +1,8 @@ package com.sourcegraph.cody.edit.widget -import org.jetbrains.annotations.VisibleForTesting import java.awt.FontMetrics import java.awt.Graphics2D +import org.jetbrains.annotations.VisibleForTesting class LensLabel(group: LensWidgetGroup, @VisibleForTesting val text: String) : LensWidget(group) { override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = fontMetrics.stringWidth(text) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index ec47aa562..11575c3db 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -19,7 +19,6 @@ import com.intellij.openapi.util.Disposer import com.intellij.ui.Gray import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.FixupSession -import org.jetbrains.annotations.NotNull import java.awt.Cursor import java.awt.Font import java.awt.FontMetrics @@ -29,6 +28,7 @@ import java.awt.geom.Rectangle2D import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Supplier +import org.jetbrains.annotations.NotNull operator fun Point.component1() = this.x @@ -270,9 +270,7 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : if (ApplicationManager.getApplication().isDispatchThread) { executeAndComplete() } else { - ApplicationManager.getApplication().invokeLater { - executeAndComplete() - } + ApplicationManager.getApplication().invokeLater { executeAndComplete() } } return result } diff --git a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt index 2ea125639..a47e453e2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt +++ b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt @@ -62,7 +62,15 @@ class PostStartupActivity : StartupActivity.DumbAware { actionId: String, group: DefaultActionGroup ) { - // If the group already contains the action, do nothing. - actionManager.getAction(actionId)?.let { group.add(it, Constraints.FIRST) } + try { + // We get called multiple times on startup, so make it idempotent. + actionManager.getAction(actionId)?.let { + if (!group.getChildren(null).contains(it)) { + group.add(it, Constraints.FIRST) + } + } + } catch (x: Exception) { + logger.warn("Failed to add action $actionId to group $group", x) + } } } From 1a946f7cd1b90cd439e20830099054531798ba34 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 4 Apr 2024 21:31:12 -0700 Subject: [PATCH 17/58] lowered wait timeouts --- .../kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt | 2 +- src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 31e2cc05b..3fe686fd2 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -161,6 +161,6 @@ public class Foo { // TODO: find the lowest value this can be for production, and use it // If it's too low the test may be flaky. - const val ASYNC_WAIT_TIMEOUT_SECONDS = 50L // 5L for non-debugging + const val ASYNC_WAIT_TIMEOUT_SECONDS = 15L // 5L for non-debugging } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 0de29a700..ea8e2e592 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -72,7 +72,7 @@ class CodyAgentService(project: Project) : Disposable { fun startAgent(project: Project): CompletableFuture { ApplicationManager.getApplication().executeOnPooledThread { try { - val agent = CodyAgent.create(project).get(45, TimeUnit.SECONDS) + val agent = CodyAgent.create(project).get(10, TimeUnit.SECONDS) if (!agent.isConnected()) { val msg = "Failed to connect to agent Cody agent" logger.error(msg) From 9eaa635a110e73949a48e8dcbff463532972bf26 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 5 Apr 2024 14:20:54 -0700 Subject: [PATCH 18/58] some better error logging on timeouts --- .../com/sourcegraph/cody/agent/CodyAgentService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index ea8e2e592..d3627c13f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -72,11 +72,17 @@ class CodyAgentService(project: Project) : Disposable { fun startAgent(project: Project): CompletableFuture { ApplicationManager.getApplication().executeOnPooledThread { try { - val agent = CodyAgent.create(project).get(10, TimeUnit.SECONDS) + val future = + CodyAgent.create(project).exceptionally { err -> + val msg = "Creating agent unsuccessful: ${err.localizedMessage}" + logger.error(msg) + throw (CodyAgentException(msg)) + } + val agent = future.get(10, TimeUnit.SECONDS) if (!agent.isConnected()) { val msg = "Failed to connect to agent Cody agent" logger.error(msg) - codyAgent.completeExceptionally(CodyAgentException(msg)) + throw CodyAgentException(msg) // This will be caught by the catch blocks below } else { synchronized(startupActions) { startupActions.forEach { action -> action(agent) } } codyAgent.complete(agent) @@ -94,7 +100,6 @@ class CodyAgentService(project: Project) : Disposable { codyAgent.completeExceptionally(CodyAgentException(msg, e)) } } - return codyAgent } From 8cd8ffdca37074cc46bc82addb184e0557426cb9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 5 Apr 2024 22:36:18 -0700 Subject: [PATCH 19/58] Got integration test working again Issue was that Agent should not be in integrationi-test mode --- .../com/sourcegraph/cody/edit/DocumentCodeTest.kt | 3 ++- .../kotlin/com/sourcegraph/cody/agent/CodyAgent.kt | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 3fe686fd2..68c4fe4a2 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -161,6 +161,7 @@ public class Foo { // TODO: find the lowest value this can be for production, and use it // If it's too low the test may be flaky. - const val ASYNC_WAIT_TIMEOUT_SECONDS = 15L // 5L for non-debugging + // const val ASYNC_WAIT_TIMEOUT_SECONDS = 10000L //debug + const val ASYNC_WAIT_TIMEOUT_SECONDS = 5L } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 4a941bc6a..95afdae8d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -97,10 +97,12 @@ private constructor( val conn = startAgentProcess() val client = CodyAgentClient() client.onSetConfigFeatures = project.service() + logger.warn("Starting json-rpc Launcher") val launcher = startAgentLauncher(conn, client) val server = launcher.remoteProxy val listeningToJsonRpc = launcher.startListening() try { + logger.warn("Calling server.initialize") return server .initialize( ClientInfo( @@ -158,8 +160,8 @@ private constructor( } if (ConfigUtil.isIntegrationTestModeEnabled()) { - processBuilder.environment()["CODY_TESTING"] = "true" - processBuilder.environment()["CODY_SHIM_TESTING"] = "true" + // processBuilder.environment()["CODY_TESTING"] = "true" + // processBuilder.environment()["CODY_SHIM_TESTING"] = "true" val testToken = System.getenv("CODY_INTEGRATION_TEST_TOKEN") // The Cody side will use the real LLM if this token is present, // so you can run the integration tests against a prod LLM rather than a mock. @@ -175,7 +177,10 @@ private constructor( .redirectErrorStream(false) .redirectError(ProcessBuilder.Redirect.PIPE) .start() - process.onExit().thenAccept { token.abort() } + process.onExit().thenAccept { + logger.warn("Cody agent process exited with code " + it.exitValue()) + token.abort() + } // Redirect agent stderr into idea.log by buffering line by line into `logger.warn()` // statements. Without this logic, the stderr output of the agent process is lost if From 16df5169ab54693595a3a4fbdd410231a7f79816 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 6 Apr 2024 14:40:04 -0700 Subject: [PATCH 20/58] improved a comment --- src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index 9dcee145e..cd1d40453 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -44,10 +44,12 @@ object ConfigUtil { /** * Returns true if the specified feature flag is enabled. Feature flags are currently set in the * environment variable CODY_JETBRAINS_FEATURES. The format is - * CODY_JETBRAINS_FEATURES=cody.feature.1=true,cody.feature.2=false. The value should be unquoted - * in your run configuration, but quoted in the env var; e.g., * ``` - * export CODY_JETBRAINS_FEATURES="cody.feature.1=true,cody.feature.2=false" + * CODY_JETBRAINS_FEATURES=cody.feature.1=true,cody.feature.2=false + * ``` + * For instance: + * ``` + * export CODY_JETBRAINS_FEATURES=cody.feature.inline-edits=true * ``` * * @param flagName The name of the feature flag From f8b8235d383d439a06e158f9475a6abee783ddcd Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 6 Apr 2024 23:15:14 -0700 Subject: [PATCH 21/58] some hygiene and misc fixes --- .../com/sourcegraph/cody/edit/DocumentCodeTest.kt | 12 ------------ .../kotlin/com/sourcegraph/cody/agent/CodyAgent.kt | 4 ++-- .../com/sourcegraph/cody/agent/CodyAgentService.kt | 12 +++++------- .../cody/autocomplete/CodyEditorFactoryListener.kt | 6 +++--- src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt | 2 ++ 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 68c4fe4a2..ed5c6fbf5 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -100,18 +100,6 @@ class DocumentCodeTest : BasePlatformTestCase() { startOffset == caret && endOffset == caret) } - // Next up: - // - test Cancel - // - test Accept - // - test workspace/edit - // - assertTrue(myFixture.editor.document.text.contains("/\\*")) - // - test Undo - - // fun testTopLevelClass() { - // assert(true) - // // ... - // } - private fun configureFixture() { // spotless:off myFixture.configureByText( diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 95afdae8d..c5efb3165 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -160,8 +160,7 @@ private constructor( } if (ConfigUtil.isIntegrationTestModeEnabled()) { - // processBuilder.environment()["CODY_TESTING"] = "true" - // processBuilder.environment()["CODY_SHIM_TESTING"] = "true" + // N.B. Do not set CODY_TESTING=true -- that is for Agent-side tests. val testToken = System.getenv("CODY_INTEGRATION_TEST_TOKEN") // The Cody side will use the real LLM if this token is present, // so you can run the integration tests against a prod LLM rather than a mock. @@ -314,6 +313,7 @@ private constructor( } } + // TODO: This has not yet been tested on Windows. private fun killAgentOnWindows() { val port = NODE_INSPECT_DEFAULT_PORT val findProcessCommand = "netstat -aon | findstr :$port" diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index d3627c13f..69859e5e3 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -78,7 +78,7 @@ class CodyAgentService(project: Project) : Disposable { logger.error(msg) throw (CodyAgentException(msg)) } - val agent = future.get(10, TimeUnit.SECONDS) + val agent = future.get(45, TimeUnit.SECONDS) if (!agent.isConnected()) { val msg = "Failed to connect to agent Cody agent" logger.error(msg) @@ -88,13 +88,11 @@ class CodyAgentService(project: Project) : Disposable { codyAgent.complete(agent) CodyStatusService.resetApplication(project) } - } catch (e: TimeoutException) { - val msg = "Failed to start Cody agent in timely manner, please run any Cody action to retry" - logger.warn(msg, e) - setAgentError(project, msg) - codyAgent.completeExceptionally(CodyAgentException(msg, e)) } catch (e: Exception) { - val msg = "Failed to start Cody agent" + val msg = + if (e is TimeoutException) + "Failed to start Cody agent in timely manner, please run any Cody action to retry" + else "Failed to start Cody agent" logger.error(msg, e) setAgentError(project, msg) codyAgent.completeExceptionally(CodyAgentException(msg, e)) diff --git a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt index a738647c2..f8e235979 100644 --- a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt @@ -70,9 +70,9 @@ class CodyEditorFactoryListener : EditorFactoryListener { if (commandName == VIM_EXIT_INSERT_MODE_ACTION) { return } - // TODO: This is sending a redundant textDocument/didOpen to the Agent when first opening a - // file. - // Util.informAgentAboutEditorChange(e.editor) + // TODO: This is sending a redundant textDocument/didOpen to the Agent + // when first opening a file. + Util.informAgentAboutEditorChange(e.editor) val suggestions = instance val editor = e.editor if (isEditorValidForAutocomplete(editor) && Util.isSelectedEditor(editor)) { diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index cd1d40453..96c4751af 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -44,9 +44,11 @@ object ConfigUtil { /** * Returns true if the specified feature flag is enabled. Feature flags are currently set in the * environment variable CODY_JETBRAINS_FEATURES. The format is + * * ``` * CODY_JETBRAINS_FEATURES=cody.feature.1=true,cody.feature.2=false * ``` + * * For instance: * ``` * export CODY_JETBRAINS_FEATURES=cody.feature.inline-edits=true From a7243dd69f0954cfb66013224ae13dc4a65e323d Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 6 Apr 2024 23:33:17 -0700 Subject: [PATCH 22/58] some refactoring of the pubsub code --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index ed5c6fbf5..d5b23e92d 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -26,7 +26,9 @@ class DocumentCodeTest : BasePlatformTestCase() { } fun testGetsWorkingGroupLens() { - val foldingRangeFuture = listenForFoldingRangeReply() + // Do this before starting the edit operation, in order to be subscribed before + // the synchronous notification is sent, because it is not buffered or queued anywhere. + val foldingRangeFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES) val editor = myFixture.editor assertFalse(editor.inlayModel.hasBlockElements()) @@ -41,7 +43,11 @@ class DocumentCodeTest : BasePlatformTestCase() { assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) // This is done now. - runInEdtAndWait { testSelectionRange(foldingRangeFuture.get()) } + runInEdtAndWait { + val rangeContext = foldingRangeFuture.get() + assertNotNull(rangeContext) + testSelectionRange(rangeContext!!) + } // Lens group should match the expected structure. val lenses = context!!.session.lensGroup @@ -64,21 +70,6 @@ class DocumentCodeTest : BasePlatformTestCase() { runInEdtAndWait { Disposer.dispose(lenses) } } - private fun listenForFoldingRangeReply(): - CompletableFuture { - val foldingRangeFuture = CompletableFuture() - project.messageBus - .connect() - .subscribe( - CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES, - object : CodyInlineEditActionNotifier { - override fun afterAction(context: CodyInlineEditActionNotifier.Context) { - foldingRangeFuture.complete(context) - } - }) - return foldingRangeFuture - } - @RequiresEdt private fun testSelectionRange(context: CodyInlineEditActionNotifier.Context) { // TODO: Test/check selection range & fail test @@ -125,23 +116,29 @@ public class Foo { """) // spotless:on } - // Block until the passed topic gets a message, or until we time out. - private fun waitForTopic( - topic: Topic - ): CodyInlineEditActionNotifier.Context? { + private fun subscribeToTopic( + topic: Topic, + ): CompletableFuture { val future = - CompletableFuture() - .completeOnTimeout(null, ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + CompletableFuture() + .completeOnTimeout(null, ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) project.messageBus .connect() .subscribe( - topic, - object : CodyInlineEditActionNotifier { - override fun afterAction(context: CodyInlineEditActionNotifier.Context) { - future.complete(context) - } - }) - return future.get() + topic, + object : CodyInlineEditActionNotifier { + override fun afterAction(context: CodyInlineEditActionNotifier.Context) { + future.complete(context) + } + }) + return future + } + + // Block until the passed topic gets a message, or until we time out. + private fun waitForTopic( + topic: Topic + ): CodyInlineEditActionNotifier.Context? { + return subscribeToTopic(topic).get() } companion object { From 9665434cb51aeeacfa226b60c3dd3bb7cb1eb0b1 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 7 Apr 2024 13:43:00 -0700 Subject: [PATCH 23/58] renamed intTest to integrationTest There was a naming conflict before that has been resolved --- CONTRIBUTING.md | 2 +- build.gradle.kts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77d5c85c3..05a3995ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -399,7 +399,7 @@ see the configuration dropdown at the top. Run the integration tests at the command line with: ``` -./gradlew intTest +./gradlew integrationTest ``` If you pass in an access token, it will use the default production LLM diff --git a/build.gradle.kts b/build.gradle.kts index 448b20e53..92d2e4599 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -98,7 +98,7 @@ java { } } -tasks.named("classpathIndexCleanup") { dependsOn("compileIntTestKotlin") } +tasks.named("classpathIndexCleanup") { dependsOn("compileIntegrationTestKotlin") } fun download(url: String, output: File) { if (output.exists()) { @@ -417,12 +417,12 @@ tasks { test { dependsOn(project.tasks.getByPath("buildCody")) } configurations { - create("intTestImplementation") { extendsFrom(configurations.testImplementation.get()) } - create("intTestRuntimeClasspath") { extendsFrom(configurations.testRuntimeOnly.get()) } + create("integrationTestImplementation") { extendsFrom(configurations.testImplementation.get()) } + create("integrationTestRuntimeClasspath") { extendsFrom(configurations.testRuntimeOnly.get()) } } sourceSets { - create("intTest") { + create("integrationTest") { kotlin.srcDir("src/integrationTest/kotlin") compileClasspath += main.get().output runtimeClasspath += main.get().output @@ -430,13 +430,13 @@ tasks { } // Create a task to run integration tests - register("intTest") { + register("integrationTest") { description = "Runs the integration tests." group = "verification" - testClassesDirs = sourceSets["intTest"].output.classesDirs - classpath = sourceSets["intTest"].runtimeClasspath + testClassesDirs = sourceSets["integrationTest"].output.classesDirs + classpath = sourceSets["integrationTest"].runtimeClasspath - include { it.file.hasParentNamed("intTest") } + include { it.file.hasParentNamed("integrationTest") } useJUnit() @@ -450,5 +450,5 @@ tasks { dependsOn("buildCody") } - named("check") { dependsOn("intTest") } + named("check") { dependsOn("integrationTest") } } From 829e6ab4afeadf5a2ff9c8c8094c9dc96c25ff17 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 12 Apr 2024 09:54:38 -0700 Subject: [PATCH 24/58] removed code to kill existing agent process For some reason it was killing my own process, before I actually got a chance to start the process. Led to failed Agent startup when using --inspect; fixed. --- .../com/sourcegraph/cody/agent/CodyAgent.kt | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index c5efb3165..13f292cb0 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -10,13 +10,13 @@ import com.intellij.util.system.CpuArch import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.cody.vscode.CancellationToken import com.sourcegraph.config.ConfigUtil +import org.eclipse.lsp4j.jsonrpc.Launcher import java.io.* import java.net.Socket import java.net.URI import java.nio.file.* import java.util.* import java.util.concurrent.* -import org.eclipse.lsp4j.jsonrpc.Launcher /** * Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for @@ -129,7 +129,6 @@ private constructor( } private fun startAgentProcess(): AgentConnection { - killAnyCompetingDebugAgentProcess() if (ConfigUtil.shouldConnectToDebugAgent()) { return connectToDebugAgent() } @@ -294,53 +293,5 @@ private constructor( val port = System.getenv("CODY_AGENT_DEBUG_PORT")?.toInt() ?: DEFAULT_AGENT_DEBUG_PORT return AgentConnection.SocketConnection(Socket("localhost", port)) } - - // Default port used when you pass --inspect or --inspect-brk to a node.js process. - private const val NODE_INSPECT_DEFAULT_PORT = 9229 - - private fun killAnyCompetingDebugAgentProcess() { - // We only kill an Agent process if it's on our port; i.e., a leftover zombie. - if (shouldSpawnDebuggableAgent()) { - try { - if (SystemInfoRt.isWindows) { - killAgentOnWindows() - } else { - killAgentOnUnix() - } - } catch (e: Exception) { - logger.warn("failed trying to kill existing agent process", e) - } - } - } - - // TODO: This has not yet been tested on Windows. - private fun killAgentOnWindows() { - val port = NODE_INSPECT_DEFAULT_PORT - val findProcessCommand = "netstat -aon | findstr :$port" - val processBuilder = - ProcessBuilder("cmd.exe", "/c", findProcessCommand).redirectErrorStream(true) - val process = processBuilder.start() - val processOutput = process.inputStream.bufferedReader().readText().trim() - val pid = processOutput.substringAfterLast(" ").trim() - if (pid.isNotEmpty()) { - logger.warn("Killing process pid=$pid on port $port") - Runtime.getRuntime().exec("taskkill /F /PID $pid") - } - } - - private fun killAgentOnUnix() { - val port = NODE_INSPECT_DEFAULT_PORT - val pid = - Runtime.getRuntime() - .exec("lsof -ti tcp:$port") - .inputStream - .bufferedReader() - .readText() - .trim() - if (pid.isNotEmpty()) { - logger.warn("Killing process on port $port with PID $pid") - Runtime.getRuntime().exec("kill $pid") - } - } } } From 288eeb463d41e33bc9f33d45e4dd27fee4e537fa Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 11 Apr 2024 18:53:05 -0700 Subject: [PATCH 25/58] minor refactoring --- .../com/sourcegraph/cody/agent/CodyAgent.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 882fb56c3..f05cea60a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -158,17 +158,8 @@ private constructor( processBuilder.environment()["CODY_LOG_EVENT_MODE"] = "connected-instance-only" } - if (ConfigUtil.isIntegrationTestModeEnabled()) { - // N.B. Do not set CODY_TESTING=true -- that is for Agent-side tests. - val testToken = System.getenv("CODY_INTEGRATION_TEST_TOKEN") - // The Cody side will use the real LLM if this token is present, - // so you can run the integration tests against a prod LLM rather than a mock. - if (testToken is String && testToken.isNotBlank()) { - processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = testToken - } else { - logger.warn("No access token passed for integration tests; using mock LLM") - } - } + configureIntegrationTesting(processBuilder) + logger.warn(processBuilder.environment().toString()) val process = processBuilder @@ -191,6 +182,20 @@ private constructor( return AgentConnection.ProcessConnection(process) } + private fun configureIntegrationTesting(processBuilder: ProcessBuilder) { + // N.B. Do not set CODY_TESTING=true -- that is for Agent-side tests. + if (!ConfigUtil.isIntegrationTestModeEnabled()) return + + val testToken = System.getenv("CODY_INTEGRATION_TEST_TOKEN") + // The Cody side will use the real LLM if this token is present, + // so you can run the integration tests against a prod LLM rather than a mock. + if (testToken is String && testToken.isNotBlank()) { + processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = testToken + } else { + logger.warn("No access token passed for integration tests") + } + } + @Throws(IOException::class, CodyAgentException::class) private fun startAgentLauncher( process: AgentConnection, From 766b4630b19a1da2d599569da8fc9400ca342da9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 16 Apr 2024 09:00:25 -0700 Subject: [PATCH 26/58] more work on Document Code integration test --- build.gradle.kts | 20 ++++-- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 63 +++++++++++++++++-- .../com/sourcegraph/cody/agent/CodyAgent.kt | 30 ++++++++- .../sourcegraph/cody/agent/CodyAgentServer.kt | 2 +- .../cody/agent/CodyAgentService.kt | 11 +++- .../cody/edit/CodyInlineEditActionNotifier.kt | 12 ++++ .../cody/edit/DocumentCodeSession.kt | 2 - .../com/sourcegraph/cody/edit/FixupSession.kt | 18 ++++++ .../cody/edit/widget/LensAction.kt | 2 +- .../com/sourcegraph/config/ConfigUtil.kt | 6 +- 10 files changed, 143 insertions(+), 23 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 523c75564..ff533a7aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,7 @@ import com.jetbrains.plugin.structure.base.utils.isDirectory +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL import java.nio.file.FileSystems import java.nio.file.FileVisitResult @@ -12,9 +15,6 @@ import java.util.* import java.util.jar.JarFile import java.util.zip.ZipFile import kotlin.script.experimental.jvm.util.hasParentNamed -import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String) = project.findProperty(key).toString() @@ -172,7 +172,7 @@ fun unzip(input: File, output: File, excludeMatcher: PathMatcher? = null) { } } -val githubArchiveCache = +val githubArchiveCache: File = Paths.get(System.getProperty("user.home"), ".sourcegraph", "caches", "jetbrains").toFile() tasks { @@ -430,6 +430,8 @@ tasks { } // Create a task to run integration tests + // Make a new integration test recording with + // ./gradlew integrationTest -PrecordingMode=record register("integrationTest") { description = "Runs the integration tests." group = "verification" @@ -445,7 +447,15 @@ tasks { "idea.test.execution.policy", // For now, should be used by all tests "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") - environment("CODY_TESTING", "true") + val recordingMode = project.findProperty("recordingMode")?.toString() ?: "replay" + environment("CODY_RECORDING_MODE", recordingMode) + // Polly replays recorded responses from here during the tests. + environment("CODY_RECORDING_DIRECTORY", "recordings") + if (recordingMode == "record") { + environment("CODY_RECORDING_NAME", "integration-tests") + environment("CODY_RECORD_IF_MISSING", "true") // Polly needs this to record at all. + } + environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") dependsOn("buildCody") } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index d5b23e92d..45d6e56cb 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -12,6 +12,7 @@ import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit +import java.util.regex.Pattern class DocumentCodeTest : BasePlatformTestCase() { @@ -42,7 +43,7 @@ class DocumentCodeTest : BasePlatformTestCase() { // The inlay should be up. assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) - // This is done now. + // We've finished receiving the folding range by now. runInEdtAndWait { val rangeContext = foldingRangeFuture.get() assertNotNull(rangeContext) @@ -70,9 +71,58 @@ class DocumentCodeTest : BasePlatformTestCase() { runInEdtAndWait { Disposer.dispose(lenses) } } + fun testShowsAcceptLens() { + val editor = myFixture.editor + assertFalse(editor.inlayModel.hasBlockElements()) + + runInEdtAndWait { EditorTestUtil.executeAction(editor, "cody.documentCodeAction") } + + val context = waitForTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) + assertNotNull("Timed out waiting for Accept group lens", context) + + // The inlay should be up. + assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) + + // Lens group should match the expected structure. + val lenses = context!!.session.lensGroup + 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.command == FixupSession.COMMAND_ACCEPT }) + assertNotNull("Lens group should contain Show Diff action", + widgets.find { widget -> widget is LensAction && widget.command == FixupSession.COMMAND_DIFF }) + assertNotNull("Lens group should contain Show Undo action", + widgets.find { widget -> widget is LensAction && widget.command == FixupSession.COMMAND_UNDO }) + assertNotNull("Lens group should contain Show Retry action", + widgets.find { widget -> widget is LensAction && widget.command == FixupSession.COMMAND_RETRY }) + + // Make sure a doc comment was inserted. + assertTrue(hasJavadocComment(editor.document.text)) + + // This avoids an error saying the spinner hasn't shut down, at the end of the test. + runInEdtAndWait { Disposer.dispose(lenses) } + } + + fun testUndo() { + // Kick off an 'editCommands/document' request. + val editor = myFixture.editor + runInEdtAndWait { EditorTestUtil.executeAction(editor, "cody.documentCodeAction") } + + val undoFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_PERFORM_UNDO) + + // The Accept/Retry/Undo group is now showing. + // TODO: Send the Undo action as if the user triggered it. + // - we will need to turn our pseudo-actions into real IDE actions + + val context = undoFuture.get() + assertNotNull("Timed out waiting for Undo action", context) + } + @RequiresEdt private fun testSelectionRange(context: CodyInlineEditActionNotifier.Context) { - // TODO: Test/check selection range & fail test // Ensure we were able to get the selection range. val selection = context.session.selectionRange assertNotNull("Selection should have been set", selection) @@ -141,12 +191,17 @@ public class Foo { return subscribeToTopic(topic).get() } + private fun hasJavadocComment(text: String): Boolean { + // TODO: Check for the exact contents once they are frozen. + val javadocPattern = Pattern.compile("/\\*\\*.*?\\*/", Pattern.DOTALL) + return javadocPattern.matcher(text).find() + } + companion object { // TODO: find the lowest value this can be for production, and use it // If it's too low the test may be flaky. - // const val ASYNC_WAIT_TIMEOUT_SECONDS = 10000L //debug - const val ASYNC_WAIT_TIMEOUT_SECONDS = 5L + const val ASYNC_WAIT_TIMEOUT_SECONDS = 15L } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index f05cea60a..e935fc58b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -159,7 +159,6 @@ private constructor( } configureIntegrationTesting(processBuilder) - logger.warn(processBuilder.environment().toString()) val process = processBuilder @@ -186,9 +185,34 @@ private constructor( // N.B. Do not set CODY_TESTING=true -- that is for Agent-side tests. if (!ConfigUtil.isIntegrationTestModeEnabled()) return + processBuilder.environment().apply { + this["CODY_RECORDING_NAME"] = "integration-test" + // N.B. If you set CODY_RECORDING_MODE, you must set CODY_RECORDING_DIRECTORY, + // or the Agent will throw an error and your test will fail. + when (val mode = System.getenv("CODY_RECORDING_MODE")) { + null -> { + logger.warn( + """Polly is not enabled for this test. + Set CODY_RECORDING_MODE and CODY_RECORDING_DIRECTORY + variables to turn on Polly.""" + .trimMargin()) + } + "record", + "replay", + "passthrough" -> { + logger.warn("Cody recording mode: $mode") + this["CODY_RECORDING_MODE"] = mode + this["CODY_RECORD_IF_MISSING"] = "true" + // This flag is for Agent-side integration testing and interferes with ours. + // It seems to be sneaking in somewhere, so we explicitly set it to false. + this["CODY_TESTING"] = "false" + this["CODY_RECORDING_DIRECTORY"] = "recordings" + } + else -> throw CodyAgentException("Unknown CODY_RECORDING_MODE: $mode") + } + } + val testToken = System.getenv("CODY_INTEGRATION_TEST_TOKEN") - // The Cody side will use the real LLM if this token is present, - // so you can run the integration tests against a prod LLM rather than a mock. if (testToken is String && testToken.isNotBlank()) { processBuilder.environment()["CODY_INTEGRATION_TEST_TOKEN"] = testToken } else { diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt index aa3ffadcb..36affd18e 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt @@ -80,7 +80,7 @@ interface CodyAgentServer { @JsonRequest("commands/smell") fun commandsSmell(): CompletableFuture - @JsonRequest("commands/document") fun commandsDocument(): CompletableFuture + @JsonRequest("editCommands/document") fun commandsDocument(): CompletableFuture @JsonRequest("editCommands/code") fun commandsEdit(params: InlineEditParams): CompletableFuture diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index f22372c12..9dd9d5aca 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -47,9 +47,16 @@ class CodyAgentService(project: Project) : Disposable { FixupService.getInstance(project).getSessionForTask(task)?.update(task) } + agent.client.onEditTaskDidDelete = Consumer { task -> + FixupService.getInstance(project).getSessionForTask(task)?.taskDeleted() + } + + agent.client.onWorkspaceEdit = Consumer { params -> + FixupService.getInstance(project).getActiveSession()?.performWorkspaceEdit(params) + } + agent.client.onTextDocumentEdit = Consumer { params -> - // I think we're only using workspace/edit now -- namespace change? - logger.warn("textDocument/edit not supported in JetBrains") + FixupService.getInstance(project).getActiveSession()?.performInlineEdits(params.edits) } if (!project.isDisposed) { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt index 9957decbc..5a884dcf7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt @@ -25,6 +25,18 @@ interface CodyInlineEditActionNotifier { Topic.create( "Sourcegraph Cody: Cody working lens shown", CodyInlineEditActionNotifier::class.java) + @JvmStatic + @Topic.ProjectLevel + val TOPIC_DISPLAY_ACCEPT_GROUP = + Topic.create( + "Sourcegraph Cody: Accept lens shown", CodyInlineEditActionNotifier::class.java) + + /** Sent when the user selects the Undo action and the edits are discarded. */ + @JvmStatic + @Topic.ProjectLevel + val TOPIC_PERFORM_UNDO = + Topic.create("Sourcegraph Cody: Undo Inline Edit", CodyInlineEditActionNotifier::class.java) + /** Sent after a workspace/edit is applied. */ @JvmStatic @Topic.ProjectLevel diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeSession.kt index cc3d88a4a..01325199f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeSession.kt @@ -33,6 +33,4 @@ class DocumentCodeSession( // You can see it in action now by clicking the green gutter to the left of Cody changes. logger.warn("Code Lenses: Show Diff") } - - override fun dispose() {} } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index bfef6d36d..9ec5cc0f3 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -72,10 +72,26 @@ abstract class FixupSession( init { triggerDocumentCodeAsync() + // Kotlin doesn't like leaking 'this' before the constructors are finished. + ApplicationManager.getApplication().invokeLater { + Disposer.register(controller, this) + } } fun commandCallbacks(): Map Unit> = lensActionCallbacks + override fun dispose() { + lensGroup?.dispose() + lensGroup = null + rangeMarkers.forEach { it.dispose() } + rangeMarkers.clear() + try { + fixupService.removeSession(this) + } catch (x: Exception) { + logger.warn("Error while removing session", x) + } + } + @RequiresEdt private fun triggerDocumentCodeAsync() { // This caret lookup requires us to be on the EDT. @@ -220,6 +236,7 @@ abstract class FixupSession( @RequiresBackgroundThread private fun showAcceptGroup() { showLensGroup(LensGroupFactory(this).createAcceptGroup()) + publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) } fun finish() { @@ -263,6 +280,7 @@ abstract class FixupSession( agent.server.commandExecute(CommandExecuteParams(COMMAND_UNDO, listOf(taskId!!))) } undoEdits() + publishProgress(CodyInlineEditActionNotifier.TOPIC_PERFORM_UNDO) finish() } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt index 3eb3e7523..b095d713c 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt @@ -12,7 +12,7 @@ import java.awt.geom.Rectangle2D class LensAction( group: LensWidgetGroup, private val text: String, - private val command: String, + val command: String, private val onClick: () -> Unit ) : LensWidget(group) { diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index 96c4751af..2d845d665 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -183,9 +183,5 @@ object ConfigUtil { } @JvmStatic - fun isIntegrationTestModeEnabled(): Boolean { - // TODO: Figure out which one to use and stick with it. - return java.lang.Boolean.getBoolean("cody.integration.testing") || - System.getenv("CODY_TESTING") == "true" - } + fun isIntegrationTestModeEnabled() = java.lang.Boolean.getBoolean("cody.integration.testing") } From 27f4b5e205104ed0036e1707e7e527976851b6b0 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 16 Apr 2024 09:42:49 -0700 Subject: [PATCH 27/58] removed @-mention instructions for now --- .../kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt index 79124f78c..5b9ca13d4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt @@ -281,7 +281,9 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, val di companion object { const val DEFAULT_TEXT_FIELD_WIDTH: Int = 620 // TODO: make this smarter - const val GHOST_TEXT = "Instructions (@ to include code)" + // TODO: Put this back when @-includes are in + //const val GHOST_TEXT = "Instructions (@ to include code)" + const val GHOST_TEXT = "Type your instructions here" // Going with a global history for now, shared across edit-code prompts. val promptHistory = mutableListOf() From 6d30b56044920c70b4c4332324df5e1ad6383c0a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 16 Apr 2024 12:38:05 -0700 Subject: [PATCH 28/58] added some more integration tests, and refactoring --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 131 +++++++++--------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 45d6e56cb..c2258cc72 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -4,7 +4,6 @@ import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.runInEdtAndWait -import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic import com.sourcegraph.cody.edit.widget.LensAction import com.sourcegraph.cody.edit.widget.LensGroupFactory @@ -26,29 +25,43 @@ class DocumentCodeTest : BasePlatformTestCase() { super.tearDown() } - fun testGetsWorkingGroupLens() { - // Do this before starting the edit operation, in order to be subscribed before - // the synchronous notification is sent, because it is not buffered or queued anywhere. + fun testGetsFoldingRanges() { val foldingRangeFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES) + executeDocumentCodeAction() + runInEdtAndWait { + val rangeContext = foldingRangeFuture.get() + assertNotNull(rangeContext) + // Ensure we were able to get the selection range. + val selection = rangeContext!!.session.selectionRange + assertNotNull("Selection should have been set", selection) + // We set the selection range to whatever the protocol returns. + // If a 0-width selection turns out to be reasonable we can adjust or remove this test. + assertFalse("Selection range should not be zero-width", selection!!.start == selection.end) + // A more robust check is to see if the selection "range" is just the caret position. + // If so, then our fallback range somehow made the round trip, which is bad. The lenses will + // go + // in the wrong places, etc. + val document = myFixture.editor.document + val startOffset = selection.start.toOffset(document) + val endOffset = selection.end.toOffset(document) + val caret = myFixture.editor.caretModel.primaryCaret.offset + assertFalse( + "Selection range should not equal the caret position", + startOffset == caret && endOffset == caret) + } + } - val editor = myFixture.editor - assertFalse(editor.inlayModel.hasBlockElements()) - - // Execute the action and await the working group lens. - runInEdtAndWait { EditorTestUtil.executeAction(editor, "cody.documentCodeAction") } + fun testGetsWorkingGroupLens() { + val future = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) + executeDocumentCodeAction() - val context = waitForTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) + // Wait for the working group. + val context = future.get() assertNotNull("Timed out waiting for working group lens", context) // The inlay should be up. - assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) - - // We've finished receiving the folding range by now. - runInEdtAndWait { - val rangeContext = foldingRangeFuture.get() - assertNotNull(rangeContext) - testSelectionRange(rangeContext!!) - } + assertTrue( + "Lens group inlay should be displayed", myFixture.editor.inlayModel.hasBlockElements()) // Lens group should match the expected structure. val lenses = context!!.session.lensGroup @@ -72,15 +85,13 @@ class DocumentCodeTest : BasePlatformTestCase() { } fun testShowsAcceptLens() { - val editor = myFixture.editor - assertFalse(editor.inlayModel.hasBlockElements()) - - runInEdtAndWait { EditorTestUtil.executeAction(editor, "cody.documentCodeAction") } - - val context = waitForTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) + val future = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) + executeDocumentCodeAction() // awaits sending command to Agent + val context = future.get() // awaits the Accept group appearing assertNotNull("Timed out waiting for Accept group lens", context) // The inlay should be up. + val editor = myFixture.editor assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) // Lens group should match the expected structure. @@ -90,14 +101,26 @@ class DocumentCodeTest : BasePlatformTestCase() { 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.command == FixupSession.COMMAND_ACCEPT }) - assertNotNull("Lens group should contain Show Diff action", - widgets.find { widget -> widget is LensAction && widget.command == FixupSession.COMMAND_DIFF }) - assertNotNull("Lens group should contain Show Undo action", - widgets.find { widget -> widget is LensAction && widget.command == FixupSession.COMMAND_UNDO }) - assertNotNull("Lens group should contain Show Retry action", - widgets.find { widget -> widget is LensAction && widget.command == FixupSession.COMMAND_RETRY }) + assertNotNull( + "Lens group should contain Accept action", + widgets.find { widget -> + widget is LensAction && widget.command == FixupSession.COMMAND_ACCEPT + }) + assertNotNull( + "Lens group should contain Show Diff action", + widgets.find { widget -> + widget is LensAction && widget.command == FixupSession.COMMAND_DIFF + }) + assertNotNull( + "Lens group should contain Show Undo action", + widgets.find { widget -> + widget is LensAction && widget.command == FixupSession.COMMAND_UNDO + }) + assertNotNull( + "Lens group should contain Show Retry action", + widgets.find { widget -> + widget is LensAction && widget.command == FixupSession.COMMAND_RETRY + }) // Make sure a doc comment was inserted. assertTrue(hasJavadocComment(editor.document.text)) @@ -107,11 +130,9 @@ class DocumentCodeTest : BasePlatformTestCase() { } fun testUndo() { - // Kick off an 'editCommands/document' request. - val editor = myFixture.editor - runInEdtAndWait { EditorTestUtil.executeAction(editor, "cody.documentCodeAction") } - val undoFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_PERFORM_UNDO) + val editor = myFixture.editor + executeDocumentCodeAction() // The Accept/Retry/Undo group is now showing. // TODO: Send the Undo action as if the user triggered it. @@ -121,24 +142,10 @@ class DocumentCodeTest : BasePlatformTestCase() { assertNotNull("Timed out waiting for Undo action", context) } - @RequiresEdt - private fun testSelectionRange(context: CodyInlineEditActionNotifier.Context) { - // Ensure we were able to get the selection range. - val selection = context.session.selectionRange - assertNotNull("Selection should have been set", selection) - // We set the selection range to whatever the protocol returns. - // If a 0-width selection turns out to be reasonable we can adjust or remove this test. - assertFalse("Selection range should not be zero-width", selection!!.start == selection.end) - // A more robust check is to see if the selection "range" is just the caret position. - // If so, then our fallback range somehow made the round trip, which is bad. The lenses will go - // in the wrong places, etc. - val document = myFixture.editor.document - val startOffset = selection.start.toOffset(document) - val endOffset = selection.end.toOffset(document) - val caret = myFixture.editor.caretModel.primaryCaret.offset - assertFalse( - "Selection range should not equal the caret position", - startOffset == caret && endOffset == caret) + private fun executeDocumentCodeAction() { + assertFalse(myFixture.editor.inlayModel.hasBlockElements()) + // Execute the action and await the working group lens. + runInEdtAndWait { EditorTestUtil.executeAction(myFixture.editor, "cody.documentCodeAction") } } private fun configureFixture() { @@ -170,17 +177,17 @@ public class Foo { topic: Topic, ): CompletableFuture { val future = - CompletableFuture() - .completeOnTimeout(null, ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + CompletableFuture() + .completeOnTimeout(null, ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) project.messageBus .connect() .subscribe( - topic, - object : CodyInlineEditActionNotifier { - override fun afterAction(context: CodyInlineEditActionNotifier.Context) { - future.complete(context) - } - }) + topic, + object : CodyInlineEditActionNotifier { + override fun afterAction(context: CodyInlineEditActionNotifier.Context) { + future.complete(context) + } + }) return future } From f32ec5aa21bd4b39146ffaf78a0eb422d042066a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 16 Apr 2024 19:22:02 -0700 Subject: [PATCH 29/58] some refactoring of the build test targets --- build.gradle.kts | 50 +++++++++++++------ .../sourcegraph/cody/edit/DocumentCodeTest.kt | 2 + .../com/sourcegraph/cody/agent/CodyAgent.kt | 1 - .../com/sourcegraph/cody/edit/FixupSession.kt | 1 - .../cody/edit/widget/LensWidgetGroup.kt | 16 ++++-- 5 files changed, 49 insertions(+), 21 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ff533a7aa..471dccab6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ + import com.jetbrains.plugin.structure.base.utils.isDirectory import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel @@ -358,6 +359,7 @@ tasks { systemProperty( "cody.autocomplete.enableFormatting", project.property("cody.autocomplete.enableFormatting") ?: "true") + environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") val platformRuntimeVersion = project.findProperty("platformRuntimeVersion") if (platformRuntimeVersion != null) { @@ -429,11 +431,8 @@ tasks { } } - // Create a task to run integration tests - // Make a new integration test recording with - // ./gradlew integrationTest -PrecordingMode=record - register("integrationTest") { - description = "Runs the integration tests." + // Common configuration for integration tests. + val sharedIntegrationTestConfig: Test.() -> Unit = { group = "verification" testClassesDirs = sourceSets["integrationTest"].output.classesDirs classpath = sourceSets["integrationTest"].runtimeClasspath @@ -444,21 +443,42 @@ tasks { systemProperty("cody.integration.testing", "true") systemProperty( - "idea.test.execution.policy", // For now, should be used by all tests - "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") + "idea.test.execution.policy", // For now, should be used by all tests + "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") - val recordingMode = project.findProperty("recordingMode")?.toString() ?: "replay" - environment("CODY_RECORDING_MODE", recordingMode) - // Polly replays recorded responses from here during the tests. - environment("CODY_RECORDING_DIRECTORY", "recordings") - if (recordingMode == "record") { - environment("CODY_RECORDING_NAME", "integration-tests") - environment("CODY_RECORD_IF_MISSING", "true") // Polly needs this to record at all. - } environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") + environment("CODY_RECORDING_MODE", "replay") + environment("CODY_RECORDING_DIRECTORY", "recordings") + environment("CODY_RECORD_IF_MISSING", "false") // Polly needs this to record at all. dependsOn("buildCody") } + register("integrationTest") { + description = "Runs the integration tests." + applySharedConfiguration(sharedIntegrationTestConfig) + } + + // Make sure to set CODY_INTEGRATION_TEST_TOKEN when using this task. + register("passthroughIntegrationTest") { + description = "Runs the integration tests, passing everything through to the LLM." + applySharedConfiguration(sharedIntegrationTestConfig) + environment("CODY_RECORDING_MODE", "passthrough") + } + + // Make sure to set CODY_INTEGRATION_TEST_TOKEN when using this task. + register("recordingIntegrationTest") { + description = "Runs the integration tests and records the responses." + applySharedConfiguration(sharedIntegrationTestConfig) + + environment("CODY_RECORDING_MODE", "record") + environment("CODY_RECORDING_NAME", "integration-tests") + environment("CODY_RECORD_IF_MISSING", "true") // Polly needs this to record at all. + } + named("check") { dependsOn("integrationTest") } } + +fun Test.applySharedConfiguration(sharedConfig: Test.() -> Unit) { + sharedConfig() +} diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index c2258cc72..23df6f5ee 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -1,5 +1,6 @@ package com.sourcegraph.cody.edit +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -14,6 +15,7 @@ import java.util.concurrent.TimeUnit import java.util.regex.Pattern class DocumentCodeTest : BasePlatformTestCase() { + private val logger = Logger.getInstance(DocumentCodeTest::class.java) override fun setUp() { super.setUp() diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index e935fc58b..4c84382e2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -97,7 +97,6 @@ private constructor( val conn = startAgentProcess() val client = CodyAgentClient() client.onSetConfigFeatures = project.service() - logger.warn("Starting json-rpc Launcher") val launcher = startAgentLauncher(conn, client) val server = launcher.remoteProxy val listeningToJsonRpc = launcher.startListening() diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index 9ec5cc0f3..1d95d1cc2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -180,7 +180,6 @@ abstract class FixupSession( } fun update(task: EditTask) { - logger.warn("Task updated: $task") when (task.state) { // This is an internal state (parked/ready tasks) and we should never see it. CodyTaskState.Idle -> {} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 11575c3db..ad404762b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -19,6 +19,7 @@ import com.intellij.openapi.util.Disposer import com.intellij.ui.Gray import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.FixupSession +import org.jetbrains.annotations.NotNull import java.awt.Cursor import java.awt.Font import java.awt.FontMetrics @@ -28,7 +29,6 @@ import java.awt.geom.Rectangle2D import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Supplier -import org.jetbrains.annotations.NotNull operator fun Point.component1() = this.x @@ -243,9 +243,17 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : override fun dispose() { isDisposed.set(true) if (editor.isDisposed) return - editor.removeEditorMouseListener(mouseClickListener) - editor.removeEditorMouseMotionListener(mouseMotionListener) - disposeInlay() + try { + editor.removeEditorMouseListener(mouseClickListener) + editor.removeEditorMouseMotionListener(mouseMotionListener) + } catch (e: Exception) { + logger.warn("Error removing mouse listeners", e) + } + try { + disposeInlay() + } catch (e: Exception) { + logger.warn("Error disposing inlay", e) + } } private fun disposeInlay() { From ae67ae206df6d50c627b934c2ae0019421d01ba2 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 17 Apr 2024 11:13:10 -0700 Subject: [PATCH 30/58] put telemetry in testing mode for integration tests This fixes a problem where all the dotcom requests were failing --- src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 4c84382e2..0982940c2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -186,6 +186,7 @@ private constructor( processBuilder.environment().apply { this["CODY_RECORDING_NAME"] = "integration-test" + this["CODY_TELEMETRY_EXPORTER"] = "testing" // N.B. If you set CODY_RECORDING_MODE, you must set CODY_RECORDING_DIRECTORY, // or the Agent will throw an error and your test will fail. when (val mode = System.getenv("CODY_RECORDING_MODE")) { From 9778ff21de95e81cc5958bc600cc94b6aba02ddc Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 17 Apr 2024 22:10:50 -0700 Subject: [PATCH 31/58] more work on integration tests and inline edits Turned the lenses into first-class actions and made many other fixes --- build.gradle.kts | 13 +- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 65 +++++++--- .../com/sourcegraph/cody/agent/CodyAgent.kt | 8 +- .../cody/agent/CodyAgentService.kt | 6 +- .../autocomplete/CodyEditorFactoryListener.kt | 4 + .../sourcegraph/cody/edit/CodeLensActions.kt | 59 +++++++++ .../cody/edit/CodyInlineEditActionNotifier.kt | 7 ++ .../cody/edit/EditCommandPrompt.kt | 4 +- .../com/sourcegraph/cody/edit/FixupService.kt | 114 ++---------------- .../com/sourcegraph/cody/edit/FixupSession.kt | 56 ++++++--- .../cody/edit/widget/LensAction.kt | 39 +++++- .../cody/edit/widget/LensGroupFactory.kt | 16 ++- .../sourcegraph/cody/edit/widget/LensLabel.kt | 7 +- .../cody/edit/widget/LensWidget.kt | 2 +- .../cody/edit/widget/LensWidgetGroup.kt | 39 ++++-- src/main/resources/META-INF/plugin.xml | 33 ++++- .../cody/agent/CurrentConfigFeaturesTest.kt | 2 +- 17 files changed, 292 insertions(+), 182 deletions(-) create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/CodeLensActions.kt diff --git a/build.gradle.kts b/build.gradle.kts index 471dccab6..999c3b914 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,4 @@ - import com.jetbrains.plugin.structure.base.utils.isDirectory -import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL import java.nio.file.FileSystems import java.nio.file.FileVisitResult @@ -16,6 +12,9 @@ import java.util.* import java.util.jar.JarFile import java.util.zip.ZipFile import kotlin.script.experimental.jvm.util.hasParentNamed +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String) = project.findProperty(key).toString() @@ -58,6 +57,8 @@ dependencies { testImplementation("org.awaitility:awaitility-kotlin:4.2.0") testImplementation("org.junit.jupiter:junit-jupiter:5.7.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.22") + implementation("org.mockito:mockito-all:1.10.19") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1") } spotless { @@ -443,8 +444,8 @@ tasks { systemProperty("cody.integration.testing", "true") systemProperty( - "idea.test.execution.policy", // For now, should be used by all tests - "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") + "idea.test.execution.policy", // For now, should be used by all tests + "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") environment("CODY_RECORDING_MODE", "replay") diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 23df6f5ee..861ef2394 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -1,5 +1,8 @@ package com.sourcegraph.cody.edit +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil @@ -13,6 +16,7 @@ import com.sourcegraph.cody.edit.widget.LensSpinner import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern +import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) @@ -23,6 +27,13 @@ class DocumentCodeTest : BasePlatformTestCase() { } override fun tearDown() { + FixupService.getInstance(myFixture.project).getActiveSession()?.apply { + try { + finish() + } catch (x: Exception) { + logger.warn("Error shutting down session", x) + } + } // TODO: Notify the Agent that all documents were closed. super.tearDown() } @@ -81,23 +92,23 @@ class DocumentCodeTest : BasePlatformTestCase() { assertTrue( "Fifth lens should be a label with a hotkey", (widgets[5] as LensLabel).text.matches(Regex(" \\(.+\\)"))) - - // This avoids an error saying the spinner hasn't shut down, at the end of the test. - runInEdtAndWait { Disposer.dispose(lenses) } } - fun testShowsAcceptLens() { + private fun awaitAcceptLensGroup(): CodyInlineEditActionNotifier.Context { val future = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) executeDocumentCodeAction() // awaits sending command to Agent val context = future.get() // awaits the Accept group appearing assertNotNull("Timed out waiting for Accept group lens", context) - - // The inlay should be up. val editor = myFixture.editor assertTrue("Lens group inlay should be displayed", editor.inlayModel.hasBlockElements()) + return context!! + } + + fun testShowsAcceptLens() { + val context = awaitAcceptLensGroup() // Lens group should match the expected structure. - val lenses = context!!.session.lensGroup + val lenses = context.session.lensGroup assertNotNull("Lens group should be displayed", lenses) val widgets = lenses!!.widgets @@ -125,20 +136,35 @@ class DocumentCodeTest : BasePlatformTestCase() { }) // Make sure a doc comment was inserted. - assertTrue(hasJavadocComment(editor.document.text)) + assertTrue(hasJavadocComment(myFixture.editor.document.text)) // This avoids an error saying the spinner hasn't shut down, at the end of the test. runInEdtAndWait { Disposer.dispose(lenses) } } + fun testAccept() { + val project = myFixture.project!! + assertNull(FixupService.getInstance(project).getActiveSession()) + + awaitAcceptLensGroup() + assertTrue(myFixture.editor.inlayModel.hasBlockElements()) + assertNotNull(FixupService.getInstance(project).getActiveSession()) + + val future = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_PERFORM_ACCEPT) + triggerAction(FixupSession.ACTION_ACCEPT) + + val context = future.get() + assertNotNull("Timed out waiting for Accept action to complete", context) + + assertFalse(myFixture.editor.inlayModel.hasBlockElements()) + assertNull(FixupService.getInstance(project).getActiveSession()) + } + fun testUndo() { val undoFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_PERFORM_UNDO) - val editor = myFixture.editor executeDocumentCodeAction() - // The Accept/Retry/Undo group is now showing. - // TODO: Send the Undo action as if the user triggered it. - // - we will need to turn our pseudo-actions into real IDE actions + triggerAction(FixupSession.ACTION_UNDO) val context = undoFuture.get() assertNotNull("Timed out waiting for Undo action", context) @@ -193,11 +219,16 @@ public class Foo { return future } - // Block until the passed topic gets a message, or until we time out. - private fun waitForTopic( - topic: Topic - ): CodyInlineEditActionNotifier.Context? { - return subscribeToTopic(topic).get() + private fun triggerAction(actionId: String) { + val action = ActionManager.getInstance().getAction(actionId) + action.actionPerformed( + AnActionEvent( + null, + mock(DataContext::class.java), + "", + action.templatePresentation.clone(), + ActionManager.getInstance(), + 0)) } private fun hasJavadocComment(text: String): Boolean { diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 0982940c2..abfdbd4a7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -10,13 +10,13 @@ import com.intellij.util.system.CpuArch import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.cody.vscode.CancellationToken import com.sourcegraph.config.ConfigUtil -import org.eclipse.lsp4j.jsonrpc.Launcher import java.io.* import java.net.Socket import java.net.URI import java.nio.file.* import java.util.* import java.util.concurrent.* +import org.eclipse.lsp4j.jsonrpc.Launcher /** * Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for @@ -192,15 +192,15 @@ private constructor( when (val mode = System.getenv("CODY_RECORDING_MODE")) { null -> { logger.warn( - """Polly is not enabled for this test. - Set CODY_RECORDING_MODE and CODY_RECORDING_DIRECTORY + """Polly is not enabled for this test. + Set CODY_RECORDING_MODE and CODY_RECORDING_DIRECTORY variables to turn on Polly.""" .trimMargin()) } "record", "replay", "passthrough" -> { - logger.warn("Cody recording mode: $mode") + logger.warn("Cody integration test recording mode: $mode") this["CODY_RECORDING_MODE"] = mode this["CODY_RECORD_IF_MISSING"] = "true" // This flag is for Agent-side integration testing and interferes with ours. diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 9dd9d5aca..b5757380e 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -44,11 +44,11 @@ class CodyAgentService(project: Project) : Disposable { } agent.client.onEditTaskDidUpdate = Consumer { task -> - FixupService.getInstance(project).getSessionForTask(task)?.update(task) + FixupService.getInstance(project).getActiveSession()?.update(task) } - agent.client.onEditTaskDidDelete = Consumer { task -> - FixupService.getInstance(project).getSessionForTask(task)?.taskDeleted() + agent.client.onEditTaskDidDelete = Consumer { _ -> + FixupService.getInstance(project).getActiveSession()?.taskDeleted() } agent.client.onWorkspaceEdit = Consumer { params -> diff --git a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt index f8e235979..859ab153a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt @@ -26,6 +26,7 @@ import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.autocomplete.CodyAutocompleteManager.Companion.instance import com.sourcegraph.cody.autocomplete.action.AcceptCodyAutocompleteAction import com.sourcegraph.cody.chat.CodeEditorFactory +import com.sourcegraph.cody.edit.FixupService import com.sourcegraph.cody.vscode.InlineCompletionTriggerKind import com.sourcegraph.config.ConfigUtil.isCodyEnabled import com.sourcegraph.telemetry.GraphQlLogger @@ -101,6 +102,9 @@ class CodyEditorFactoryListener : EditorFactoryListener { if (!Util.isSelectedEditor(editor)) { return } + editor.project?.let { project -> + FixupService.getInstance(project).getActiveSession()?.handleDocumentChange(editor) + } val completions = instance completions.clearAutocompleteSuggestions(editor) if (isImplicitAutocompleteEnabledForEditor(editor) && diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodeLensActions.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodeLensActions.kt new file mode 100644 index 000000000..bf3c26967 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodeLensActions.kt @@ -0,0 +1,59 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project + +// This file contains a bunch of tiny dispatcher classes for Inline Edits. +// They provide a bridge between our fake code-lens widgets and IntelliJ's +// action system, which supports hotkeys, integration tests, and such. + +abstract class CodeLensAction : AnAction() { + private val logger = Logger.getInstance(CodeLensAction::class.java) + + override fun actionPerformed(e: AnActionEvent) { + var project = e.project + if (project == null) { + project = e.dataContext.getData(PlatformDataKeys.PROJECT.name) as? Project + } + if (project == null || project.isDisposed) { + logger.warn("Received EditCancelAction for null or disposed project: $project") + return + } + performAction(e, project) + } + + abstract fun performAction(e: AnActionEvent, project: Project) +} + +class EditCancelAction : CodeLensAction() { + override fun performAction(e: AnActionEvent, project: Project) { + FixupService.getInstance(project).getActiveSession()?.cancel() + } +} + +class EditAcceptAction : CodeLensAction() { + override fun performAction(e: AnActionEvent, project: Project) { + FixupService.getInstance(project).getActiveSession()?.accept() + } +} + +class EditRetryAction : CodeLensAction() { + override fun performAction(e: AnActionEvent, project: Project) { + FixupService.getInstance(project).getActiveSession()?.retry() + } +} + +class EditDiffAction : CodeLensAction() { + override fun performAction(e: AnActionEvent, project: Project) { + FixupService.getInstance(project).getActiveSession()?.diff() + } +} + +class EditUndoAction : CodeLensAction() { + override fun performAction(e: AnActionEvent, project: Project) { + FixupService.getInstance(project).getActiveSession()?.undo() + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt index 5a884dcf7..bfd41ee7a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt @@ -37,6 +37,13 @@ interface CodyInlineEditActionNotifier { val TOPIC_PERFORM_UNDO = Topic.create("Sourcegraph Cody: Undo Inline Edit", CodyInlineEditActionNotifier::class.java) + /** Sent when the user performs the Accept action and the edits are kept. */ + @JvmStatic + @Topic.ProjectLevel + val TOPIC_PERFORM_ACCEPT = + Topic.create( + "Sourcegraph Cody: Accept Inline Edit", CodyInlineEditActionNotifier::class.java) + /** Sent after a workspace/edit is applied. */ @JvmStatic @Topic.ProjectLevel diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt index 5b9ca13d4..d4a34aaf4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt @@ -190,7 +190,7 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, val di logger.warn("Project was null when trying to add an edit session") return } - controller.addSession( + controller.setActiveSession( EditSession(controller, editor, project, editor.document, text, llmDropdown.item)) } } @@ -282,7 +282,7 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, val di const val DEFAULT_TEXT_FIELD_WIDTH: Int = 620 // TODO: make this smarter // TODO: Put this back when @-includes are in - //const val GHOST_TEXT = "Instructions (@ to include code)" + // const val GHOST_TEXT = "Instructions (@ to include code)" const val GHOST_TEXT = "Type your instructions here" // Going with a global history for now, shared across edit-code prompts. diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt index 4d91dcac7..d1f7ab09a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt @@ -6,92 +6,20 @@ import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.CodyAgentService -import com.sourcegraph.cody.agent.protocol.EditTask +import com.intellij.openapi.util.Disposer import com.sourcegraph.config.ConfigUtil.isCodyEnabled import com.sourcegraph.utils.CodyEditorUtil -import java.util.function.Consumer /** Controller for commands that allow the LLM to edit the code directly. */ @Service(Service.Level.PROJECT) class FixupService(val project: Project) : Disposable { private val logger = Logger.getInstance(FixupService::class.java) - // We only use this for multiplexing task updates from the Agent to concurrent sessions. - // TODO: Consider doing the multiplexing in CodyAgentClient instead. - private var activeSessions: MutableMap = mutableMapOf() - - // Sessions for which we have not yet received a task ID, but may receive an edit anyway. - private var pendingSessions: MutableSet = mutableSetOf() + private var activeSession: FixupSession? = null // The last text the user typed in without saving it, for continuity. private var lastPrompt: String = "" - init { - // JetBrains docs say avoid heavy lifting in the constructor, so pass to another thread. - CodyAgentService.withAgent(project) { agent -> - agent.client.onEditTaskDidUpdate = Consumer { task -> - val session = activeSessions[task.id] - if (session == null) { - logger.warn("onEditTaskDidUpdate: No session found for task ${task.id}") - } else { - session.update(task) - } - } - - agent.client.onEditTaskDidDelete = Consumer { task -> - val session = activeSessions[task.id] - if (session == null) { - logger.warn("onEditTaskDidDelete: No session found for task ${task.id}") - } else { - session.taskDeleted() - } - } - - agent.client.onWorkspaceEdit = Consumer { params -> - for (op in params.operations) { - // TODO: We need to support the file-level operations. - when (op.type) { - "create-file" -> { - logger.warn("Workspace edit operation created a file: ${op.uri}") - } - "rename-file" -> { - logger.warn("Workspace edit operation renamed a file: ${op.oldUri} -> ${op.newUri}") - } - "delete-file" -> { - logger.warn("Workspace edit operation deleted a file: ${op.uri}") - } - "edit-file" -> { - if (op.edits == null) { - logger.warn("Workspace edit operation has no edits") - } else { - // If there is a pending session, assume that it is the one that caused the - // edit. - val session: FixupSession? = - if (pendingSessions.isNotEmpty()) { - pendingSessions.firstOrNull() // I still see empty collections here (race?) - } else { - // TODO: This is what I'd like to be able to do, but it requires a - // protocol change: - // session = activeSessions[op.id] - activeSessions.values.firstOrNull() - } - if (session == null) { - logger.warn("No sessions found for performing inline edits") - } else { - session.performInlineEdits(op.edits) - } - } - } - else -> - logger.warn( - "DocumentCommand session received unknown workspace edit operation: ${op.type}") - } - } - } - } - } - /** Entry point for the inline edit command, called by the action handler. */ fun startCodeEdit(editor: Editor) { if (isEligibleForInlineEdit(editor)) { @@ -119,41 +47,23 @@ class FixupService(val project: Project) : Disposable { fun getLastPrompt(): String = lastPrompt - fun getActiveSession(): FixupSession? { - val session: FixupSession? = - pendingSessions.firstOrNull() ?: activeSessions.values.firstOrNull() - if (session == null) { - logger.warn("No sessions found for performing inline edits") - } - return session - } + fun getActiveSession(): FixupSession? = activeSession - fun getSessionForTask(task: EditTask): FixupSession? { - val session = activeSessions[task.id] - if (session == null) { - logger.warn("No session found for task ${task.id}") - } - return session + fun setActiveSession(session: FixupSession) { + if (session == activeSession) return + clearActiveSession() + activeSession = session } - fun addSession(session: FixupSession) { - val taskId = session.taskId - if (taskId == null) { - pendingSessions.add(session) - } else { - pendingSessions.remove(session) - activeSessions[session.taskId!!] = session + fun clearActiveSession() { + if (activeSession != null) { + logger.warn("Setting new session when previous session is active: $activeSession") } - } - - fun removeSession(session: FixupSession) { - pendingSessions.remove(session) - activeSessions.remove(session.taskId) + activeSession = null } override fun dispose() { - activeSessions.values.forEach { it.dispose() } - pendingSessions.forEach { it.dispose() } + activeSession?.let { Disposer.dispose(it) } } companion object { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt index 1d95d1cc2..560cfff8a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupSession.kt @@ -35,6 +35,7 @@ import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean /** * Common functionality for commands that let the agent edit the code inline, such as adding a doc @@ -61,6 +62,8 @@ abstract class FixupSession( private var rangeMarkers: MutableSet = mutableSetOf() + private val showedAcceptLens = AtomicBoolean(false) + private val lensActionCallbacks = mapOf( COMMAND_ACCEPT to { accept() }, @@ -73,20 +76,22 @@ abstract class FixupSession( init { triggerDocumentCodeAsync() // Kotlin doesn't like leaking 'this' before the constructors are finished. - ApplicationManager.getApplication().invokeLater { - Disposer.register(controller, this) - } + ApplicationManager.getApplication().invokeLater { Disposer.register(controller, this) } } fun commandCallbacks(): Map Unit> = lensActionCallbacks override fun dispose() { - lensGroup?.dispose() - lensGroup = null + try { + lensGroup?.dispose() + lensGroup = null + } catch (x: Exception) { + logger.warn("Error while disposing lenses", x) + } rangeMarkers.forEach { it.dispose() } rangeMarkers.clear() try { - fixupService.removeSession(this) + fixupService.clearActiveSession() } catch (x: Exception) { logger.warn("Error while removing session", x) } @@ -102,13 +107,13 @@ abstract class FixupSession( ensureSelectionRange(agent, caret) showWorkingGroup() // All this because we can get the workspace/edit before the request returns! - fixupService.addSession(this) // puts in Pending + fixupService.setActiveSession(this) makeEditingRequest(agent) .handle { result, error -> if (error != null || result == null) { // TODO: Adapt logic from CodyCompletionsManager.handleError logger.warn("Error while generating doc string: $error") - fixupService.removeSession(this) + fixupService.clearActiveSession() } else { taskId = result.id // Sometimes even though we get a folding range back, we don't get a selection range. @@ -117,7 +122,7 @@ abstract class FixupSession( } else { selectionRange = result.selectionRange } - fixupService.addSession(this) + fixupService.setActiveSession(this) } null } @@ -125,7 +130,7 @@ abstract class FixupSession( if (!(error is CancellationException || error is CompletionException)) { logger.warn("Error while generating doc string: $error") } - fixupService.removeSession(this) + finish() null } .completeOnTimeout(null, 3, TimeUnit.SECONDS) @@ -235,18 +240,31 @@ abstract class FixupSession( @RequiresBackgroundThread private fun showAcceptGroup() { showLensGroup(LensGroupFactory(this).createAcceptGroup()) + showedAcceptLens.set(true) publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) } + fun handleDocumentChange(editorThatChanged: Editor) { + if (editorThatChanged != editor) return + // We auto-accept if they edit the document after we've put up this lens group. + if (showedAcceptLens.get()) { + accept() + } else { + cancel() + } + } + fun finish() { try { - controller.removeSession(this) - rangeMarkers.forEach { it.dispose() } - rangeMarkers.clear() + controller.clearActiveSession() } catch (x: Exception) { logger.debug("Session cleanup error", x) } - Disposer.dispose(this) + try { + Disposer.dispose(this) + } catch (x: Exception) { + logger.warn("Error disposing fixup session $this", x) + } } /** Subclass sends a fixup command to the agent, and returns the initial task. */ @@ -257,6 +275,7 @@ abstract class FixupSession( agent.server.commandExecute(CommandExecuteParams(COMMAND_ACCEPT, listOf(taskId!!))) } finish() + publishProgress(CodyInlineEditActionNotifier.TOPIC_PERFORM_ACCEPT) } fun cancel() { @@ -405,13 +424,20 @@ abstract class FixupSession( } companion object { - // Lens actions the user can take; we notify the Agent when they are taken. + // VS Code Commands that we send back to the agent. const val COMMAND_ACCEPT = "cody.fixup.codelens.accept" const val COMMAND_CANCEL = "cody.fixup.codelens.cancel" const val COMMAND_RETRY = "cody.fixup.codelens.retry" const val COMMAND_DIFF = "cody.fixup.codelens.diff" const val COMMAND_UNDO = "cody.fixup.codelens.undo" + // JetBrains Actions that we fire when the lenses are clicked. + const val ACTION_ACCEPT = "cody.inlineEditAcceptAction" + const val ACTION_CANCEL = "cody.inlineEditCancelAction" + const val ACTION_RETRY = "cody.inlineEditRetryAction" + const val ACTION_DIFF = "cody.inlineEditDiffAction" + const val ACTION_UNDO = "cody.inlineEditUndoAction" + // TODO: Register the hotkeys now that we are displaying them. fun getHotKey(command: String): String { val mac = SystemInfoRt.isMac diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt index b095d713c..053db586b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt @@ -1,11 +1,17 @@ package com.sourcegraph.cody.edit.widget +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.EditorMouseEvent import com.intellij.ui.JBColor import com.sourcegraph.cody.edit.FixupSession import java.awt.Font import java.awt.FontMetrics import java.awt.Graphics2D +import java.awt.event.MouseEvent import java.awt.font.TextAttribute import java.awt.geom.Rectangle2D @@ -13,7 +19,7 @@ class LensAction( group: LensWidgetGroup, private val text: String, val command: String, - private val onClick: () -> Unit + private val actionId: String, ) : LensWidget(group) { private val underline = mapOf(TextAttribute.UNDERLINE to TextAttribute.UNDERLINE_ON) @@ -46,8 +52,8 @@ class LensAction( } } - override fun onClick(x: Int, y: Int): Boolean { - onClick.invoke() + override fun onClick(e: EditorMouseEvent): Boolean { + triggerAction(actionId, e.editor, e.mouseEvent) return true } @@ -56,6 +62,33 @@ class LensAction( showTooltip(FixupSession.getHotKey(command), e.mouseEvent) } + private fun triggerAction(actionId: String, editor: Editor, mouseEvent: MouseEvent) { + val action = ActionManager.getInstance().getAction(actionId) + if (action != null) { + val dataContext = createDataContext(editor, mouseEvent) + val actionEvent = + AnActionEvent( + null, + dataContext, + "", + action.templatePresentation.clone(), + ActionManager.getInstance(), + 0) + action.actionPerformed(actionEvent) + } + } + + private fun createDataContext(editor: Editor, mouseEvent: MouseEvent): DataContext { + return DataContext { dataId -> + when (dataId) { + PlatformDataKeys.CONTEXT_COMPONENT.name -> mouseEvent.component + PlatformDataKeys.EDITOR.name -> editor + PlatformDataKeys.PROJECT.name -> editor.project + else -> null + } + } + } + override fun toString(): String { return "LensAction(text=$text)" } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt index 37964e824..1071d0b15 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt @@ -14,7 +14,7 @@ class LensGroupFactory(val session: FixupSession) { addSpacer(this) addLabel(this, "Cody is working...") addSeparator(this) - addAction(this, "Cancel", FixupSession.COMMAND_CANCEL) + addAction(this, "Cancel", FixupSession.COMMAND_CANCEL, FixupSession.ACTION_CANCEL) registerWidgets() } } @@ -23,13 +23,13 @@ class LensGroupFactory(val session: FixupSession) { return LensWidgetGroup(session, session.editor).apply { addLogo(this) addSpacer(this) - addAction(this, "Accept", FixupSession.COMMAND_ACCEPT) + addAction(this, "Accept", FixupSession.COMMAND_ACCEPT, FixupSession.ACTION_ACCEPT) addSeparator(this) - addAction(this, "Edit & Retry", FixupSession.COMMAND_RETRY) + addAction(this, "Edit & Retry", FixupSession.COMMAND_RETRY, FixupSession.ACTION_RETRY) addSeparator(this) - addAction(this, "Undo", FixupSession.COMMAND_UNDO) + addAction(this, "Undo", FixupSession.COMMAND_UNDO, FixupSession.ACTION_UNDO) addSeparator(this) - addAction(this, "Show Diff", FixupSession.COMMAND_DIFF) + addAction(this, "Show Diff", FixupSession.COMMAND_DIFF, FixupSession.ACTION_DIFF) registerWidgets() } } @@ -54,10 +54,8 @@ class LensGroupFactory(val session: FixupSession) { addLabel(group, ICON_SPACER) } - private fun addAction(group: LensWidgetGroup, label: String, command: String) { - val callback = - session.commandCallbacks()[command] ?: { logger.warn("No callback for $command") } - group.addWidget(LensAction(group, label, command, callback)) + private fun addAction(group: LensWidgetGroup, label: String, command: String, actionId: String) { + group.addWidget(LensAction(group, label, command, actionId)) val hotkey = FixupSession.getHotKey(command) if (hotkey.isNotEmpty()) { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt index d4b8aa1e2..2084edf46 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt @@ -1,8 +1,8 @@ package com.sourcegraph.cody.edit.widget +import org.jetbrains.annotations.VisibleForTesting import java.awt.FontMetrics import java.awt.Graphics2D -import org.jetbrains.annotations.VisibleForTesting class LensLabel(group: LensWidgetGroup, @VisibleForTesting val text: String) : LensWidget(group) { override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = fontMetrics.stringWidth(text) @@ -13,11 +13,6 @@ class LensLabel(group: LensWidgetGroup, @VisibleForTesting val text: String) : L g.drawString(text, x, y + g.fontMetrics.ascent) } - override fun onClick(x: Int, y: Int): Boolean { - // Labels do nothing when clicked. - return true - } - override fun toString(): String { return "LensLabel(text=$text)" } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt index bfc513ba7..be6a340d5 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt @@ -39,7 +39,7 @@ abstract class LensWidget(val parentGroup: LensWidgetGroup) : Disposable { open fun update() {} /** Called only when widget is clicked. Coordinates are relative to the widget. */ - open fun onClick(x: Int, y: Int): Boolean { + open fun onClick(e: EditorMouseEvent): Boolean { // Not all widgets care. return false } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index ad404762b..467532550 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.editor.impl.FontInfo import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.Disposer import com.intellij.ui.Gray +import com.intellij.util.concurrency.annotations.RequiresEdt import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.FixupSession import org.jetbrains.annotations.NotNull @@ -44,6 +45,8 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : val editor = parentComponent as EditorImpl val isDisposed = AtomicBoolean(false) + private val addedListeners = AtomicBoolean(false) + private val removedListeners = AtomicBoolean(false) val widgets = mutableListOf() @@ -93,6 +96,7 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : Disposer.register(session, this) editor.addEditorMouseListener(mouseClickListener) editor.addEditorMouseMotionListener(mouseMotionListener) + addedListeners.set(true) } fun withListenersMuted(block: () -> Unit) { @@ -210,7 +214,7 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : // Dispatch mouse click events to the appropriate widget. private fun handleMouseClick(e: EditorMouseEvent) { val (x, y) = e.mouseEvent.point - if (findWidgetAt(x, y)?.onClick(x, y) == true) { + if (findWidgetAt(x, y)?.onClick(e) == true) { e.consume() } } @@ -239,23 +243,34 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : } } - /** Immediately hides and discards this inlay and widget group. */ + /** Hides and discards this inlay and widget group. */ override fun dispose() { + // We work extra hard to ensure this method is idempotent and robust, + // because IntelliJ (annoyingly) logs an assertion if you try to remove + // a nonexistent listener, and it pops up a user-visible exception. + if (isDisposed.get()) return isDisposed.set(true) if (editor.isDisposed) return - try { - editor.removeEditorMouseListener(mouseClickListener) - editor.removeEditorMouseMotionListener(mouseMotionListener) - } catch (e: Exception) { - logger.warn("Error removing mouse listeners", e) - } - try { - disposeInlay() - } catch (e: Exception) { - logger.warn("Error disposing inlay", e) + onEventThread { + if (editor.isDisposed) return@onEventThread + if (addedListeners.get() && !removedListeners.get()) { + try { + removedListeners.set(true) + editor.removeEditorMouseListener(mouseClickListener) + editor.removeEditorMouseMotionListener(mouseMotionListener) + } catch (t: Throwable) { + logger.warn("Error removing mouse listeners", t) + } + } + try { + disposeInlay() + } catch (t: Throwable) { + logger.warn("Error disposing inlay", t) + } } } + @RequiresEdt private fun disposeInlay() { inlay?.apply { if (isValid) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 471401790..6b3a69b57 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -227,7 +227,7 @@ - + @@ -241,6 +241,37 @@ + + + + + + + + + + + + + + + + diff --git a/src/test/kotlin/com/sourcegraph/cody/agent/CurrentConfigFeaturesTest.kt b/src/test/kotlin/com/sourcegraph/cody/agent/CurrentConfigFeaturesTest.kt index 97e623fe2..9fe0243bd 100644 --- a/src/test/kotlin/com/sourcegraph/cody/agent/CurrentConfigFeaturesTest.kt +++ b/src/test/kotlin/com/sourcegraph/cody/agent/CurrentConfigFeaturesTest.kt @@ -3,7 +3,7 @@ package com.sourcegraph.cody.agent import java.util.concurrent.CopyOnWriteArrayList import org.awaitility.kotlin.await import org.awaitility.kotlin.until -import org.hamcrest.CoreMatchers.hasItems +import org.hamcrest.Matchers.hasItems import org.junit.Assert.assertEquals import org.junit.Assert.assertThat import org.junit.Test From 78de66ccec1893a1bf5ed2b72c56e8bbf90b4a6c Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 18 Apr 2024 17:21:36 -0700 Subject: [PATCH 32/58] fixed indentation --- src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt | 2 +- .../kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt index 2084edf46..8cc2ab853 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt @@ -1,8 +1,8 @@ package com.sourcegraph.cody.edit.widget -import org.jetbrains.annotations.VisibleForTesting import java.awt.FontMetrics import java.awt.Graphics2D +import org.jetbrains.annotations.VisibleForTesting class LensLabel(group: LensWidgetGroup, @VisibleForTesting val text: String) : LensWidget(group) { override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = fontMetrics.stringWidth(text) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 467532550..cd721b459 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -20,7 +20,6 @@ import com.intellij.ui.Gray import com.intellij.util.concurrency.annotations.RequiresEdt import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.FixupSession -import org.jetbrains.annotations.NotNull import java.awt.Cursor import java.awt.Font import java.awt.FontMetrics @@ -30,6 +29,7 @@ import java.awt.geom.Rectangle2D import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Supplier +import org.jetbrains.annotations.NotNull operator fun Point.component1() = this.x From 1a6793a92b8dcd921e607c4900fa8fbf548eb130 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 3 May 2024 18:26:26 -0700 Subject: [PATCH 33/58] a bit of work on error lenses --- .../cody/edit/CodyInlineEditActionNotifier.kt | 6 ++++++ .../sourcegraph/cody/edit/EditCommandPrompt.kt | 11 ++++++----- .../cody/edit/sessions/FixupSession.kt | 18 ++++++++++++++++-- .../cody/edit/widget/LensGroupFactory.kt | 10 +++++++--- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt index 1ab65a80e..c12674348 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt @@ -31,6 +31,12 @@ interface CodyInlineEditActionNotifier { Topic.create( "Sourcegraph Cody: Accept lens shown", CodyInlineEditActionNotifier::class.java) + @JvmStatic + @Topic.ProjectLevel + val TOPIC_DISPLAY_ERROR_GROUP = + Topic.create( + "Sourcegraph Cody: Error lens shown", CodyInlineEditActionNotifier::class.java) + @JvmStatic @Topic.ProjectLevel val TOPIC_PERFORM_UNDO = diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt index 4f20a50f8..4473b606c 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt @@ -81,8 +81,6 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog private val offset = editor.caretModel.primaryCaret.offset - private val escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0) - private var connection: MessageBusConnection? = null private val isDisposed: AtomicBoolean = AtomicBoolean(false) @@ -99,6 +97,8 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK) } + private val escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0) + private val okButton = namedButton("ok-button").apply { text = "Edit Code" @@ -536,9 +536,10 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog val text = instructionsField.text if (text.isNotBlank()) { promptHistory.add(text) - val project = editor.project - // TODO: How do we show user feedback when an error like this happens? - if (project == null) { + if (editor.project == null) { + controller + .getActiveSession() + ?.displayError("Error initiating Code Edit", "Could not find current Project") logger.warn("Project was null when trying to add an edit session") return } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 86b976bc3..467d20de0 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -76,6 +76,10 @@ abstract class FixupSession( private val performedActions: MutableList = mutableListOf() + private val defaultErrorText by lazy { + "Cody failed to ${if (this is DocumentCodeSession) "document" else "edit"} this code" + } + init { triggerFixupAsync() ApplicationManager.getApplication().invokeLater { Disposer.register(controller, this) } @@ -210,8 +214,18 @@ abstract class FixupSession( publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ACCEPT_GROUP) } - private fun showErrorGroup(hoverText: String) { - showLensGroup(LensGroupFactory(this).createErrorGroup(hoverText)) + private fun showErrorGroup(labelText: String, hoverText: String? = null) { + showLensGroup(LensGroupFactory(this).createErrorGroup(labelText, hoverText)) + publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ERROR_GROUP) + } + + /** + * Puts up the error lens group with the specified message and optional hover-text. + * The message should be short, no more than about 60 characters. + * The hover text can be longer and include more diagnostic information. + */ + fun displayError(text: String, hoverText: String? = null) { + showErrorGroup(text, hoverText) } // TODO: Have the current editor's DocumentListener call us. diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt index 49b50d1fb..eab2e32f0 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt @@ -36,12 +36,16 @@ class LensGroupFactory(val session: FixupSession) { } } - fun createErrorGroup(tooltip: String, isDocumentCode: Boolean = false): LensWidgetGroup { + fun createErrorGroup( + message: String, // The message to show in the error lens; should be short. + tooltip: String? = null // Can show more detail here. + ): LensWidgetGroup { return LensWidgetGroup(session, session.editor).apply { addLogo(this) addErrorIcon(this) - val verb = if (isDocumentCode) "document" else "edit" - addLabel(this, "Cody failed to $verb this code").apply { hoverText = tooltip } + if (tooltip != null) { + addLabel(this, message).apply { hoverText = tooltip } + } addSeparator(this) addAction(this, "Dismiss", FixupSession.ACTION_DISMISS) addSeparator(this) From 366e3a31584d12ab33f37009007b648afd4d9fed Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 5 May 2024 14:48:24 -0700 Subject: [PATCH 34/58] fixed some compile errors from the merge --- .../com/sourcegraph/cody/edit/DocumentCodeTest.kt | 11 ++++++----- .../com/sourcegraph/cody/edit/widget/LensAction.kt | 2 +- .../com/sourcegraph/cody/edit/widget/LensHotkey.kt | 2 +- .../com/sourcegraph/cody/edit/widget/LensLabel.kt | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 861ef2394..609dc6691 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -9,14 +9,15 @@ import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.runInEdtAndWait import com.intellij.util.messages.Topic +import com.sourcegraph.cody.edit.sessions.FixupSession import com.sourcegraph.cody.edit.widget.LensAction import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner +import org.mockito.Mockito.mock import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern -import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) @@ -117,22 +118,22 @@ class DocumentCodeTest : BasePlatformTestCase() { assertNotNull( "Lens group should contain Accept action", widgets.find { widget -> - widget is LensAction && widget.command == FixupSession.COMMAND_ACCEPT + widget is LensAction && widget.actionId == FixupSession.ACTION_ACCEPT }) assertNotNull( "Lens group should contain Show Diff action", widgets.find { widget -> - widget is LensAction && widget.command == FixupSession.COMMAND_DIFF + widget is LensAction && widget.actionId == FixupSession.ACTION_DIFF }) assertNotNull( "Lens group should contain Show Undo action", widgets.find { widget -> - widget is LensAction && widget.command == FixupSession.COMMAND_UNDO + widget is LensAction && widget.actionId == FixupSession.ACTION_UNDO }) assertNotNull( "Lens group should contain Show Retry action", widgets.find { widget -> - widget is LensAction && widget.command == FixupSession.COMMAND_RETRY + widget is LensAction && widget.actionId == FixupSession.ACTION_RETRY }) // Make sure a doc comment was inserted. diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt index 35f87a9c9..1375d7988 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt @@ -22,7 +22,7 @@ import javax.swing.UIManager class LensAction( val group: LensWidgetGroup, private val text: String, - private val actionId: String + val actionId: String ) : LensWidget(group) { private val underline = mapOf(TextAttribute.UNDERLINE to TextAttribute.UNDERLINE_ON) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt index aa6663029..0abf6053a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt @@ -6,7 +6,7 @@ import java.awt.FontMetrics import java.awt.Graphics2D @Suppress("UseJBColor") -class LensHotkey(group: LensWidgetGroup, private val text: String) : LensLabel(group, text) { +class LensHotkey(group: LensWidgetGroup, text: String) : LensLabel(group, text) { private val hotkeyHighlightColor = Color(49, 51, 56) // TODO: Put this in resources diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt index a49c65957..f74568886 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt @@ -5,7 +5,7 @@ import com.sourcegraph.cody.edit.EditCommandPrompt import java.awt.FontMetrics import java.awt.Graphics2D -open class LensLabel(group: LensWidgetGroup, private val text: String) : LensWidget(group) { +open class LensLabel(group: LensWidgetGroup, val text: String) : LensWidget(group) { override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = fontMetrics.stringWidth(text) From 05d7f4a9d33b4f4173d3dbae603a0f10f0ccdf8e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 7 May 2024 08:34:00 -0700 Subject: [PATCH 35/58] fixed an EDT error --- .../com/sourcegraph/cody/edit/sessions/FixupSession.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 467d20de0..552914201 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -215,8 +215,10 @@ abstract class FixupSession( } private fun showErrorGroup(labelText: String, hoverText: String? = null) { - showLensGroup(LensGroupFactory(this).createErrorGroup(labelText, hoverText)) - publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ERROR_GROUP) + runInEdt { + showLensGroup(LensGroupFactory(this).createErrorGroup(labelText, hoverText)) + publishProgress(CodyInlineEditActionNotifier.TOPIC_DISPLAY_ERROR_GROUP) + } } /** From e1653c3527a12a05cbdea731ae65e4fb1b13a0a2 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 7 May 2024 09:00:32 -0700 Subject: [PATCH 36/58] fixes for error lenses we no longer cancel the FixupSession until the error lens is dismissed --- .../cody/edit/sessions/FixupSession.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 552914201..bac4539ab 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -77,8 +77,8 @@ abstract class FixupSession( private val performedActions: MutableList = mutableListOf() private val defaultErrorText by lazy { - "Cody failed to ${if (this is DocumentCodeSession) "document" else "edit"} this code" - } + "Cody failed to ${if (this is DocumentCodeSession) "document" else "edit"} this code" + } init { triggerFixupAsync() @@ -107,8 +107,7 @@ abstract class FixupSession( makeEditingRequest(agent) .handle { result, error -> if (error != null || result == null) { - showErrorGroup("Error while generating doc string: $error") - fixupService.cancelActiveSession() + displayError(defaultErrorText, error?.localizedMessage) } else { taskId = result.id selectionRange = adjustToDocumentRange(result.selectionRange) @@ -118,7 +117,7 @@ abstract class FixupSession( } .exceptionally { error: Throwable? -> if (!(error is CancellationException || error is CompletionException)) { - showErrorGroup("Error while generating code: ${error?.localizedMessage}") + displayError(defaultErrorText, error?.localizedMessage) } finish() null @@ -222,12 +221,12 @@ abstract class FixupSession( } /** - * Puts up the error lens group with the specified message and optional hover-text. - * The message should be short, no more than about 60 characters. - * The hover text can be longer and include more diagnostic information. + * Puts up the error lens group with the specified message and optional hover-text. The message + * should be short, no more than about 60 characters. The hover text can be longer and include + * more diagnostic information. */ fun displayError(text: String, hoverText: String? = null) { - showErrorGroup(text, hoverText) + showErrorGroup(text, hoverText ?: "No additional info from Agent") } // TODO: Have the current editor's DocumentListener call us. From 842d42d13f62116bba6243ed8d8ea74825f54011 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 10 May 2024 15:31:29 -0700 Subject: [PATCH 37/58] initial work on integration tests --- CONTRIBUTING.md | 34 +++++++-- gradle.properties | 2 +- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 2 +- .../cody/agent/CodyAgentService.kt | 6 +- .../cody/agent/protocol/CodyError.kt | 3 + .../cody/agent/protocol/EditTask.kt | 8 +- .../cody/edit/CodyInlineEditActionNotifier.kt | 3 +- .../cody/edit/EditCommandPrompt.kt | 33 ++++---- .../com/sourcegraph/cody/edit/FixupService.kt | 18 ++--- .../cody/edit/sessions/FixupSession.kt | 76 +++++++++---------- .../cody/edit/widget/LensAction.kt | 7 +- .../cody/edit/widget/LensGroupFactory.kt | 2 +- .../cody/edit/widget/LensSpinner.kt | 4 +- .../cody/edit/widget/LensWidgetGroup.kt | 3 +- .../cody/ignore/CommandPanelIgnoreBanner.kt | 4 +- .../cody/listeners/CodyCaretListener.kt | 2 +- .../cody/listeners/CodyDocumentListener.kt | 46 ++++++----- .../cody/listeners/CodyFileEditorListener.kt | 2 +- .../cody/listeners/CodySelectionListener.kt | 2 +- .../com/sourcegraph/cody/ui/FrameMover.kt | 5 +- .../com/sourcegraph/utils/CodyEditorUtil.kt | 13 ++-- 21 files changed, 155 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyError.kt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ae16453b..cd308f7c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,7 +116,25 @@ After doing that: ## Publishing a New Release -We plan to make releases every other Monday. Nightly version can be released as often as there is a need. + +```mermaid +graph TD; + Title["JetBrains plugin release"] --> stable; + Title --> nightly; + stable --> push_stable["push git tag"]; + push_stable --> release_job_stable["wait for release job to complete"]; + release_job_stable --> marketplace_approval["wait for marketplace approval"]; + marketplace_approval -->|Automated approval, up to 48hr| unhide["unhide"]; + unhide --> available_to_end_users_stable["available for download"]; + marketplace_approval -->|Manual quick-approve| slack_approval["request JetBrains Marketplace team to manually approve update via Slack"]; + slack_approval --> unhide["unhide approved release (requires admin access)"]; + nightly --> push_nightly["push git tag\nwith '-nightly' suffix"]; + push_nightly --> release_job_nightly["wait for release job to complete"]; + release_job_nightly --> available_to_end_users_nightly["available for download"]; +``` + +We aim to cut a new Stable release every other week on Mondays. +The release cadence is irregular for Nightly versions. ### 1. Push a Git Tag @@ -148,7 +166,12 @@ After successfully pushing the new tag (for example: `v5.2.4819` or `v5.2.4249-n Wait for the `Release to Marketplace` GitHub workflow to complete. -### 2. Publish a New Release on GitHub +### 2. For Stable releases, wait for Marketplace approval + +It can take up to 48hr for stable releases to get approved by the JetBrains Marketplace team. +It's possible to expedite this process by posting a message in the `#marketplace` channel in the [JetBrains Slack workspace](https://plugins.jetbrains.com/slack/). + +### 3. Publish a New Release on GitHub For every stable release, create a GitHub release summarizing the changes. @@ -162,10 +185,11 @@ to [our first release](https://github.com/sourcegraph/jetbrains/releases/tag/v5. It's also optional create GitHub releases for nightly builds where it makes sense. -### 3. Announce the New Release on our internal Slack channel +### 4. Announce the New Release on our internal Slack channel -It is mandatory to post about both stable and nightly releases on our internal `wg-cody-jetbrains` Slack channel. You -can refer to past posts in the channel's history for examples. +It is mandatory to post about both stable and nightly releases on our internal +`#team-cody-clients` Slack channel. You can refer to past posts in the channel's +history for examples. ## Enabling web view debugging diff --git a/gradle.properties b/gradle.properties index b5a887245..5b7de40db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,4 +27,4 @@ kotlin.stdlib.default.dependency=false nodeBinaries.commit=8755ae4c05fd476cd23f2972049111ba436c86d4 nodeBinaries.version=v20.12.2 cody.autocomplete.enableFormatting=true -cody.commit=d951e25d0c84c72365bd2fffb144a3fbf1170158 +cody.commit=b74b93fa41eec5864bb7c17271916c0b7680efad diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 609dc6691..62912a94f 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -14,10 +14,10 @@ import com.sourcegraph.cody.edit.widget.LensAction import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner -import org.mockito.Mockito.mock import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern +import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 6c59976c2..4ee10f366 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -52,8 +52,10 @@ class CodyAgentService(project: Project) : Disposable { FixupService.getInstance(project).getActiveSession()?.update(task) } - agent.client.onEditTaskDidDelete = Consumer { _ -> - FixupService.getInstance(project).getActiveSession()?.taskDeleted() + agent.client.onEditTaskDidDelete = Consumer { params -> + FixupService.getInstance(project).getActiveSession()?.let { + if (params.id == it.taskId) it.taskDeleted() + } } agent.client.onWorkspaceEdit = Consumer { params -> diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyError.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyError.kt new file mode 100644 index 000000000..923df2934 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyError.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class CodyError(val message: String, val cause: CodyError? = null, val stack: String? = null) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt index 603363bfb..dc6899c17 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt @@ -1,3 +1,9 @@ package com.sourcegraph.cody.agent.protocol -data class EditTask(val id: String, val state: CodyTaskState, val selectionRange: Range) +data class EditTask( + val id: String, + val state: CodyTaskState, + val error: CodyError? = null, + val selectionRange: Range, + val instruction: String? = null +) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt index c12674348..4a89c96ab 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt @@ -34,8 +34,7 @@ interface CodyInlineEditActionNotifier { @JvmStatic @Topic.ProjectLevel val TOPIC_DISPLAY_ERROR_GROUP = - Topic.create( - "Sourcegraph Cody: Error lens shown", CodyInlineEditActionNotifier::class.java) + Topic.create("Sourcegraph Cody: Error lens shown", CodyInlineEditActionNotifier::class.java) @JvmStatic @Topic.ProjectLevel diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt index 4473b606c..73d741495 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.BulkAwareDocumentListener @@ -69,14 +70,17 @@ import javax.swing.JRootPane import javax.swing.JScrollPane import javax.swing.KeyStroke import javax.swing.ListCellRenderer -import javax.swing.SwingUtilities import javax.swing.WindowConstants import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener /** Pop up a user interface for giving Cody instructions to fix up code at the cursor. */ -class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialogTitle: String) : - JFrame(), Disposable { +class EditCommandPrompt( + val controller: FixupService, + val editor: Editor, + dialogTitle: String, + instruction: String? = null +) : JFrame(), Disposable { private val logger = Logger.getInstance(EditCommandPrompt::class.java) private val offset = editor.caretModel.primaryCaret.offset @@ -102,8 +106,6 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog private val okButton = namedButton("ok-button").apply { text = "Edit Code" - foreground = boldLabelColor() - addActionListener { performOKAction() } registerKeyboardAction( { performOKAction() }, enterKeyStroke, JComponent.WHEN_IN_FOCUSED_WINDOW) @@ -124,8 +126,12 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog private val instructionsField = InstructionsInputTextArea(this).apply { - text = lastPrompt - if (text.isBlank() && promptHistory.isNotEmpty()) { + if (instruction != null) { + text = instruction + } else { + text = lastPrompt + } + if (text.isNullOrBlank() && promptHistory.isNotEmpty()) { text = promptHistory.getPrevious() } } @@ -201,7 +207,7 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog } private fun handleDocumentChange() { - ApplicationManager.getApplication().invokeLater { + runInEdt { updateOkButtonState() checkForInterruptions() } @@ -252,8 +258,9 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog } } - // Note: Must be created on EDT, although we can't annotate it as such. init { + ApplicationManager.getApplication().assertIsDispatchThread() + // Register with FixupService as a failsafe if the project closes. Normally we're disposed // sooner, when the dialog is closed or focus is lost. Disposer.register(controller, this) @@ -538,13 +545,13 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog promptHistory.add(text) if (editor.project == null) { controller - .getActiveSession() - ?.displayError("Error initiating Code Edit", "Could not find current Project") + .getActiveSession() + ?.displayError("Error initiating Code Edit", "Could not find current Project") logger.warn("Project was null when trying to add an edit session") return } // Kick off the editing command. - controller.setActiveSession(EditCodeSession(controller, editor, text, llmDropdown.item)) + EditCodeSession(controller, editor, text, llmDropdown.item) } clearActivePrompt() } @@ -565,7 +572,7 @@ class EditCommandPrompt(val controller: FixupService, val editor: Editor, dialog } private fun onThemeChange() { - SwingUtilities.invokeLater { + runInEdt { titleLabel.foreground = boldLabelColor() // custom background we manage ourselves revalidate() repaint() diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt index 6e0a78cdb..22353b47f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicReference class FixupService(val project: Project) : Disposable { private val logger = Logger.getInstance(FixupService::class.java) - private var activeSession: FixupSession? = null + @Volatile private var activeSession: FixupSession? = null // We only have one editing session at a time in JetBrains, for now. // This reference ensures we only have one inline-edit dialog active at a time. @@ -28,14 +28,12 @@ class FixupService(val project: Project) : Disposable { /** Entry point for the inline edit command, called by the action handler. */ fun startCodeEdit(editor: Editor) { if (!isEligibleForInlineEdit(editor)) return - cancelActiveSession() currentEditPrompt.set(EditCommandPrompt(this, editor, "Edit Code with Cody")) } /** Entry point for the document code command, called by the action handler. */ fun startDocumentCode(editor: Editor) { if (!isEligibleForInlineEdit(editor)) return - activeSession?.finish() DocumentCodeSession(this, editor, editor.project ?: return) } @@ -60,19 +58,15 @@ class FixupService(val project: Project) : Disposable { fun getActiveSession(): FixupSession? = activeSession fun setActiveSession(session: FixupSession) { - if (session == activeSession) return - cancelActiveSession() + activeSession?.let { if (it.isShowingAcceptLens()) it.accept() else it.cancel() } + waitUntilActiveSessionIsFinished() activeSession = session } - // Fully cancels/retracts any current session. - fun cancelActiveSession() { - try { - activeSession?.finish() - } catch (x: Exception) { - logger.warn("Error while disposing session", x) + fun waitUntilActiveSessionIsFinished() { + while (activeSession != null) { + Thread.sleep(100) } - clearActiveSession() } fun clearActiveSession() { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index bac4539ab..ff4cbed9f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -53,7 +53,8 @@ import kotlin.io.path.toPath /** * Common functionality for commands that let the agent edit the code inline, such as adding a doc - * string, or fixing up a region according to user instructions. + * string, or fixing up a region according to user instructions. Instances of this class map 1:1 + * with FixupTask instances in the Agent. */ abstract class FixupSession( val controller: FixupService, @@ -65,7 +66,7 @@ abstract class FixupSession( private val fixupService = FixupService.getInstance(project) // This is passed back by the Agent when we initiate the editing task. - private var taskId: String? = null + @Volatile var taskId: String? = null var lensGroup: LensWidgetGroup? = null private set @@ -74,6 +75,11 @@ abstract class FixupSession( var selectionRange: Range? = null + // The prompt that the Agent used for this task. For Edit, it's the same as + // the most recent prompt the user sent, which we already have. But for Document Code, + // it enables us to show the user what we sent and let them hand-edit it. + var instruction: String? = null + private val performedActions: MutableList = mutableListOf() private val defaultErrorText by lazy { @@ -82,7 +88,7 @@ abstract class FixupSession( init { triggerFixupAsync() - ApplicationManager.getApplication().invokeLater { Disposer.register(controller, this) } + runInEdt { Disposer.register(controller, this) } } private val document @@ -90,6 +96,8 @@ abstract class FixupSession( @RequiresEdt private fun triggerFixupAsync() { + ApplicationManager.getApplication().assertIsDispatchThread() + // Those lookups require us to be on the EDT. val file = FileDocumentManager.getInstance().getFile(document) val textFile = file?.let { ProtocolTextDocument.fromVirtualFile(editor, it) } ?: return @@ -97,21 +105,20 @@ abstract class FixupSession( CodyAgentService.withAgent(project) { agent -> workAroundUninitializedCodebase() + // All this because we can get the workspace/edit before the request returns! + fixupService.setActiveSession(this) + // Spend a turn to get folding ranges before showing lenses. ensureSelectionRange(agent, textFile) showWorkingGroup() - // All this because we can get the workspace/edit before the request returns! - fixupService.setActiveSession(this) makeEditingRequest(agent) .handle { result, error -> if (error != null || result == null) { displayError(defaultErrorText, error?.localizedMessage) } else { - taskId = result.id selectionRange = adjustToDocumentRange(result.selectionRange) - fixupService.setActiveSession(this) } null } @@ -119,7 +126,7 @@ abstract class FixupSession( if (!(error is CancellationException || error is CompletionException)) { displayError(defaultErrorText, error?.localizedMessage) } - finish() + cancel() null } .completeOnTimeout(null, 3, TimeUnit.SECONDS) @@ -151,6 +158,7 @@ abstract class FixupSession( } fun update(task: EditTask) { + task.instruction?.let { instruction = it } when (task.state) { // This is an internal state (parked/ready tasks) and we should never see it. CodyTaskState.Idle -> {} @@ -159,12 +167,15 @@ abstract class FixupSession( CodyTaskState.Working, CodyTaskState.Inserting, CodyTaskState.Applying, - CodyTaskState.Formatting -> {} + CodyTaskState.Formatting -> { + taskId = task.id + } // Tasks remain in this state until explicit accept/undo/cancel. CodyTaskState.Applied -> showAcceptGroup() - // Then they transition to finished. - CodyTaskState.Finished -> {} - CodyTaskState.Error -> {} + // Then they transition to finished, or error. + CodyTaskState.Finished -> finish() + // We do not finish() until the error is displayed and closed. + CodyTaskState.Error -> displayError(defaultErrorText, task.error?.message) CodyTaskState.Pending -> {} } } @@ -247,7 +258,7 @@ abstract class FixupSession( logger.debug("Session cleanup error", x) } runInEdt { - try { + try { // Disposing inlay requires EDT. Disposer.dispose(this) } catch (x: Exception) { logger.warn("Error disposing fixup session $this", x) @@ -262,7 +273,6 @@ abstract class FixupSession( CodyAgentService.withAgent(project) { agent -> agent.server.acceptEditTask(TaskIdParam(taskId!!)) } - finish() publishProgress(CodyInlineEditActionNotifier.TOPIC_PERFORM_ACCEPT) } @@ -270,30 +280,25 @@ abstract class FixupSession( CodyAgentService.withAgent(project) { agent -> agent.server.cancelEditTask(TaskIdParam(taskId!!)) } - if (performedActions.isNotEmpty()) { - undo() - } else { - finish() + } + + fun undo() { + runInEdt { showWorkingGroup() } + CodyAgentService.withAgent(project) { agent -> + agent.server.undoEditTask(TaskIdParam(taskId!!)) } publishProgress(CodyInlineEditActionNotifier.TOPIC_PERFORM_CANCEL) } fun retry() { - ApplicationManager.getApplication().invokeLater { - // This starts an entirely new session, independent of this one. - EditCommandPrompt(controller, editor, "Edit instructions and Retry") - } - controller.cancelActiveSession() - } + val instruction = instruction - // Action handler for FixupSession.ACTION_UNDO. - fun undo() { - CodyAgentService.withAgent(project) { agent -> - agent.server.undoEditTask(TaskIdParam(taskId!!)) + undo() + + ApplicationManager.getApplication().executeOnPooledThread { + FixupService.getInstance(project).waitUntilActiveSessionIsFinished() + runInEdt { EditCommandPrompt(controller, editor, "Edit instructions and Retry", instruction) } } - undoEdits() - publishProgress(CodyInlineEditActionNotifier.TOPIC_PERFORM_UNDO) - finish() } fun dismiss() { @@ -348,7 +353,7 @@ abstract class FixupSession( CodyAgentService.withAgent(project) { agent -> ensureSelectionRange(agent, textFile) - ApplicationManager.getApplication().invokeLater { showWorkingGroup() } + runInEdt { showWorkingGroup() } } } @@ -403,13 +408,6 @@ abstract class FixupSession( return Range(start, end) } - private fun undoEdits() { - if (project.isDisposed) return - WriteCommandAction.runWriteCommandAction(project) { - performedActions.reversed().forEach { it.undo() } - } - } - fun createDiffDocument(): Document { val document = EditorFactory.getInstance().createDocument(document.text) val diffActions = performedActions.map { it.copyForDocument(document) } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt index 1375d7988..a47762670 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt @@ -19,11 +19,8 @@ import java.awt.geom.Rectangle2D import javax.swing.UIManager @Suppress("UseJBColor") -class LensAction( - val group: LensWidgetGroup, - private val text: String, - val actionId: String -) : LensWidget(group) { +class LensAction(val group: LensWidgetGroup, private val text: String, val actionId: String) : + LensWidget(group) { private val underline = mapOf(TextAttribute.UNDERLINE to TextAttribute.UNDERLINE_ON) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt index eab2e32f0..633077384 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt @@ -38,7 +38,7 @@ class LensGroupFactory(val session: FixupSession) { fun createErrorGroup( message: String, // The message to show in the error lens; should be short. - tooltip: String? = null // Can show more detail here. + tooltip: String? = null // Can show more detail here. ): LensWidgetGroup { return LensWidgetGroup(session, session.editor).apply { addLogo(this) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt index d7fc89da6..43523b9aa 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt @@ -1,6 +1,6 @@ package com.sourcegraph.cody.edit.widget -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt import java.awt.FontMetrics import java.awt.Graphics2D import java.awt.geom.AffineTransform @@ -22,7 +22,7 @@ class LensSpinner(group: LensWidgetGroup, private val icon: Icon) : LensWidget(g } init { - ApplicationManager.getApplication().invokeLater { start() } + runInEdt { start() } } fun start() { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 82f39c3b4..fbc08e979 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -2,6 +2,7 @@ package com.sourcegraph.cody.edit.widget import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorCustomElementRenderer @@ -297,7 +298,7 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : if (ApplicationManager.getApplication().isDispatchThread) { executeAndComplete() } else { - ApplicationManager.getApplication().invokeLater { executeAndComplete() } + runInEdt { executeAndComplete() } } return result } diff --git a/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt b/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt index 41a8e2b7f..43ce9e689 100644 --- a/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt +++ b/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt @@ -27,9 +27,7 @@ class CommandPanelIgnoreBanner() : NonOpaquePanel() { // These colors cribbed from EditorComposite, createTopBottomSideBorder val scheme = EditorColorsManager.getInstance().globalScheme - val borderColor = - scheme.getColor(EditorColors.SEPARATOR_ABOVE_COLOR) - ?: scheme.getColor(EditorColors.TEARLINE_COLOR) + val borderColor = scheme.getColor(EditorColors.TEARLINE_COLOR) border = SideBorder(borderColor, SideBorder.TOP or SideBorder.BOTTOM) } diff --git a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyCaretListener.kt b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyCaretListener.kt index 1da8689e4..f9b14ba22 100644 --- a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyCaretListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyCaretListener.kt @@ -25,7 +25,7 @@ class CodyCaretListener(val project: Project) : CaretListener { ProtocolTextDocument.fromEditor(e.editor)?.let { textDocument -> CodyAgentService.withAgent(project) { agent: CodyAgent -> - agent.server.textDocumentDidFocus(textDocument) + agent.server.textDocumentDidChange(textDocument) } } diff --git a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt index 3ecc2dbc6..11ffa5e39 100644 --- a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt @@ -1,6 +1,7 @@ package com.sourcegraph.cody.listeners import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.editor.event.BulkAwareDocumentListener import com.intellij.openapi.editor.event.DocumentEvent @@ -29,34 +30,37 @@ class CodyDocumentListener(val project: Project) : BulkAwareDocumentListener { } override fun documentChangedNonBulk(event: DocumentEvent) { - val editor = FileEditorManager.getInstance(project).selectedTextEditor - if (editor?.document != event.document) { - return - } + // Can be called on non-EDT during IDE shutdown. + runInEdt { + val editor = FileEditorManager.getInstance(project).selectedTextEditor + if (editor?.document != event.document) { + return@runInEdt + } - logCodeCopyPastedFromChat(event) - CodyAutocompleteManager.instance.clearAutocompleteSuggestions(editor) + logCodeCopyPastedFromChat(event) + CodyAutocompleteManager.instance.clearAutocompleteSuggestions(editor) - if (CodyEditorUtil.isImplicitAutocompleteEnabledForEditor(editor) && - CodyEditorUtil.isEditorValidForAutocomplete(editor) && - !CommandProcessor.getInstance().isUndoTransparentActionInProgress) { + if (CodyEditorUtil.isImplicitAutocompleteEnabledForEditor(editor) && + CodyEditorUtil.isEditorValidForAutocomplete(editor) && + !CommandProcessor.getInstance().isUndoTransparentActionInProgress) { - ProtocolTextDocument.fromEditor(editor)?.let { textDocument -> - CodyAgentService.withAgent(project) { agent -> - agent.server.textDocumentDidChange(textDocument) + ProtocolTextDocument.fromEditor(editor)?.let { textDocument -> + CodyAgentService.withAgent(project) { agent -> + agent.server.textDocumentDidChange(textDocument) - // This notification must be sent after the above, see tracker comment for more details. - AcceptCodyAutocompleteAction.tracker.getAndSet(null)?.let { completionID -> - agent.server.completionAccepted(CompletionItemParams(completionID)) - agent.server.autocompleteClearLastCandidate() + // This notification must be sent after the above, see tracker comment for more details. + AcceptCodyAutocompleteAction.tracker.getAndSet(null)?.let { completionID -> + agent.server.completionAccepted(CompletionItemParams(completionID)) + agent.server.autocompleteClearLastCandidate() + } } } - } - val changeOffset = event.offset + event.newLength - if (editor.caretModel.offset == changeOffset) { - CodyAutocompleteManager.instance.triggerAutocomplete( - editor, changeOffset, InlineCompletionTriggerKind.AUTOMATIC) + val changeOffset = event.offset + event.newLength + if (editor.caretModel.offset == changeOffset) { + CodyAutocompleteManager.instance.triggerAutocomplete( + editor, changeOffset, InlineCompletionTriggerKind.AUTOMATIC) + } } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyFileEditorListener.kt b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyFileEditorListener.kt index e84f95dc4..80b81ad52 100644 --- a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyFileEditorListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyFileEditorListener.kt @@ -16,7 +16,7 @@ class CodyFileEditorListener : FileEditorManagerListener { source.selectedTextEditor?.let { editor -> val protocolTextFile = fromVirtualFile(editor, file) withAgent(source.project) { agent: CodyAgent -> - agent.server.textDocumentDidClose(protocolTextFile) + agent.server.textDocumentDidOpen(protocolTextFile) } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/listeners/CodySelectionListener.kt b/src/main/kotlin/com/sourcegraph/cody/listeners/CodySelectionListener.kt index 78c98a072..ef7c638f1 100644 --- a/src/main/kotlin/com/sourcegraph/cody/listeners/CodySelectionListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/listeners/CodySelectionListener.kt @@ -16,7 +16,7 @@ class CodySelectionListener(val project: Project) : SelectionListener { ProtocolTextDocument.fromEditor(e.editor)?.let { textDocument -> CodyAgentService.withAgent(project) { agent -> - agent.server.textDocumentDidFocus(textDocument) + agent.server.textDocumentDidChange(textDocument) } } diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt b/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt index ec3036247..36489cd7f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt +++ b/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt @@ -1,6 +1,7 @@ package com.sourcegraph.cody.ui import com.intellij.openapi.Disposable +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.diagnostic.Logger import com.intellij.util.ui.UIUtil import com.sourcegraph.cody.edit.EditUtil @@ -176,7 +177,7 @@ class FrameMover(private val frame: JFrame, private val titleBar: JComponent) : else -> {} } - SwingUtilities.invokeLater { frame.setSize(newWidth, newHeight) } + runInEdt { frame.setSize(newWidth, newHeight) } lastMouseX = newX lastMouseY = newY lastUpdateTime = currentTime @@ -187,7 +188,7 @@ class FrameMover(private val frame: JFrame, private val titleBar: JComponent) : if (currentTime - lastUpdateTime > 16) { // about 60 fps val x: Int = e.xOnScreen val y: Int = e.yOnScreen - SwingUtilities.invokeLater { + runInEdt { frame.rootPane?.let { rootPane -> UIUtil.getLocationOnScreen(rootPane)?.let { loc -> frame.setLocation(loc.x + x - lastMouseX, loc.y + y - lastMouseY) diff --git a/src/main/kotlin/com/sourcegraph/utils/CodyEditorUtil.kt b/src/main/kotlin/com/sourcegraph/utils/CodyEditorUtil.kt index 223ab72b8..223386d6c 100644 --- a/src/main/kotlin/com/sourcegraph/utils/CodyEditorUtil.kt +++ b/src/main/kotlin/com/sourcegraph/utils/CodyEditorUtil.kt @@ -212,12 +212,13 @@ object CodyEditorUtil { val uri = URI.create(uriString).withScheme("file") val vf = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(uri.toPath()) ?: return false - OpenFileDescriptor( - project, - vf, - selection?.start?.line ?: 0, - /* logicalColumn= */ selection?.start?.character ?: 0) - .navigate(/* requestFocus= */ preserveFocus != true) + if (selection == null) { + OpenFileDescriptor(project, vf).navigate(/* requestFocus= */ preserveFocus != true) + } else { + OpenFileDescriptor( + project, vf, selection.start.line, /* logicalColumn= */ selection.start.character) + .navigate(/* requestFocus= */ preserveFocus != true) + } return true } catch (e: Exception) { logger.error("Cannot switch view to file $uriString", e) From 131cb36c14cd1edf86943481d0b2d7fe9b1b4fda Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 10 May 2024 15:41:08 -0700 Subject: [PATCH 38/58] auto-accept on document changes restored a method call that had gone missing --- .../kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt | 2 +- .../com/sourcegraph/cody/listeners/CodyDocumentListener.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 44cba622b..0fb1080fa 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -15,6 +15,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.messages.Topic import com.intellij.util.withScheme import com.sourcegraph.cody.agent.CodyAgent import com.sourcegraph.cody.agent.CodyAgentCodebase @@ -236,7 +237,6 @@ abstract class FixupSession( showErrorGroup(text, hoverText ?: "No additional info from Agent") } - // TODO: Have the current editor's DocumentListener call us. fun handleDocumentChange(editorThatChanged: Editor) { if (editorThatChanged != editor) return // We auto-accept if they edit the document after we've put up this lens group. diff --git a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt index 11ffa5e39..acbab712d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt @@ -13,6 +13,7 @@ import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument import com.sourcegraph.cody.autocomplete.CodyAutocompleteManager import com.sourcegraph.cody.autocomplete.action.AcceptCodyAutocompleteAction import com.sourcegraph.cody.chat.CodeEditorFactory +import com.sourcegraph.cody.edit.FixupService import com.sourcegraph.cody.vscode.InlineCompletionTriggerKind import com.sourcegraph.telemetry.GraphQlLogger import com.sourcegraph.utils.CodyEditorUtil @@ -62,6 +63,7 @@ class CodyDocumentListener(val project: Project) : BulkAwareDocumentListener { editor, changeOffset, InlineCompletionTriggerKind.AUTOMATIC) } } + FixupService.getInstance(project).getActiveSession()?.handleDocumentChange(editor) } } } From b2252b708dfc6d613bb6d1168b3038698dfc4bad Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 10 May 2024 17:51:37 -0700 Subject: [PATCH 39/58] minor refactoring cleanup in build file --- build.gradle.kts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4814e132f..f872e1e75 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,7 @@ import com.jetbrains.plugin.structure.base.utils.isDirectory +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL import java.nio.file.FileSystems import java.nio.file.FileVisitResult @@ -12,9 +15,6 @@ import java.util.* import java.util.jar.JarFile import java.util.zip.ZipFile import kotlin.script.experimental.jvm.util.hasParentNamed -import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String) = project.findProperty(key).toString() @@ -482,20 +482,20 @@ tasks { register("integrationTest") { description = "Runs the integration tests." - applySharedConfiguration(sharedIntegrationTestConfig) + sharedIntegrationTestConfig() } // Make sure to set CODY_INTEGRATION_TEST_TOKEN when using this task. register("passthroughIntegrationTest") { description = "Runs the integration tests, passing everything through to the LLM." - applySharedConfiguration(sharedIntegrationTestConfig) + sharedIntegrationTestConfig() environment("CODY_RECORDING_MODE", "passthrough") } // Make sure to set CODY_INTEGRATION_TEST_TOKEN when using this task. register("recordingIntegrationTest") { description = "Runs the integration tests and records the responses." - applySharedConfiguration(sharedIntegrationTestConfig) + sharedIntegrationTestConfig() environment("CODY_RECORDING_MODE", "record") environment("CODY_RECORDING_NAME", "integration-tests") @@ -504,7 +504,3 @@ tasks { named("check") { dependsOn("integrationTest") } } - -fun Test.applySharedConfiguration(sharedConfig: Test.() -> Unit) { - sharedConfig() -} From b5b2812ea26df73ce7a0c682c579ef73ea44c84a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 13 May 2024 11:49:10 -0700 Subject: [PATCH 40/58] more work on integration tests --- .../test-projects/document-code/.gitignore | 0 .../document-code/src/main/java/Foo.java | 0 .../sourcegraph/cody/edit/sessions/FixupSession.kt | 3 +++ .../cody/edit/widget/LensWidgetGroup.kt | 14 +++++++++----- 4 files changed, 12 insertions(+), 5 deletions(-) rename src/{test => integrationTest}/resources/test-projects/document-code/.gitignore (100%) rename src/{test => integrationTest}/resources/test-projects/document-code/src/main/java/Foo.java (100%) diff --git a/src/test/resources/test-projects/document-code/.gitignore b/src/integrationTest/resources/test-projects/document-code/.gitignore similarity index 100% rename from src/test/resources/test-projects/document-code/.gitignore rename to src/integrationTest/resources/test-projects/document-code/.gitignore diff --git a/src/test/resources/test-projects/document-code/src/main/java/Foo.java b/src/integrationTest/resources/test-projects/document-code/src/main/java/Foo.java similarity index 100% rename from src/test/resources/test-projects/document-code/src/main/java/Foo.java rename to src/integrationTest/resources/test-projects/document-code/src/main/java/Foo.java diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 0fb1080fa..ad5b3f3ab 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -69,6 +69,7 @@ abstract class FixupSession( private set private val showedAcceptLens = AtomicBoolean(false) + val isDisposed = AtomicBoolean(false) var selectionRange: Range? = null @@ -187,6 +188,7 @@ abstract class FixupSession( // integration testing, but also because there's no real harm blocking pool threads. private fun showLensGroup(group: LensWidgetGroup) { lensGroup?.let { if (!it.isDisposed.get()) Disposer.dispose(it) } + if (isDisposed.get()) return lensGroup = group var range = selectionRange if (range == null) { @@ -413,6 +415,7 @@ abstract class FixupSession( } override fun dispose() { + isDisposed.set(true) if (project.isDisposed) return performedActions.forEach { it.dispose() } } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index fbc08e979..5954398b4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -22,6 +22,7 @@ import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.EditCommandPrompt import com.sourcegraph.cody.edit.sessions.FixupSession import com.sourcegraph.config.ThemeUtil +import org.jetbrains.annotations.NotNull import java.awt.Cursor import java.awt.Font import java.awt.FontMetrics @@ -32,7 +33,6 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Supplier import kotlin.math.roundToInt -import org.jetbrains.annotations.NotNull operator fun Point.component1() = this.x @@ -97,10 +97,14 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : var isErrorGroup = false init { - Disposer.register(session, this) - editor.addEditorMouseListener(mouseClickListener) - editor.addEditorMouseMotionListener(mouseMotionListener) - addedListeners.set(true) + if (!session.isDisposed.get()) { + Disposer.register(session, this) + editor.addEditorMouseListener(mouseClickListener) + editor.addEditorMouseMotionListener(mouseMotionListener) + addedListeners.set(true) + } else { + logger.warn("Disposing lens widget group with disposed session: $this") + } } fun withListenersMuted(block: () -> Unit) { From f9697e6794d0980828f553d773a4454dbc95ca85 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 19:59:50 -0700 Subject: [PATCH 41/58] some unfinished work on integrationTest --- build.gradle.kts | 59 ++++++++++++++++++- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 53 ++++++++--------- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f872e1e75..62899ac92 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ + import com.jetbrains.plugin.structure.base.utils.isDirectory import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel @@ -390,7 +391,7 @@ tasks { systemProperty("sourcegraph.verbose-logging", "true") systemProperty( "cody.autocomplete.enableFormatting", - project.property("cody.autocomplete.enableFormatting") ?: "true") + project.property("cody.autocomplete.enableFormatting") as String? ?: "true") environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") val platformRuntimeVersion = project.findProperty("platformRuntimeVersion") @@ -463,6 +464,14 @@ tasks { testClassesDirs = sourceSets["integrationTest"].output.classesDirs classpath = sourceSets["integrationTest"].runtimeClasspath + // TODO: Refactor to share these with runIde. + systemProperty("cody-agent.trace-path", "$buildDir/sourcegraph/cody-agent-trace.json") + systemProperty("cody-agent.directory", buildCodyDir.parent) + systemProperty("sourcegraph.verbose-logging", "true") + systemProperty( + "cody.autocomplete.enableFormatting", + project.property("cody.autocomplete.enableFormatting") as String? ?: "true") + include { it.file.hasParentNamed("integrationTest") } useJUnit() @@ -482,7 +491,39 @@ tasks { register("integrationTest") { description = "Runs the integration tests." - sharedIntegrationTestConfig() + group = "verification" + testClassesDirs = sourceSets["integrationTest"].output.classesDirs + classpath = sourceSets["integrationTest"].runtimeClasspath + + // TODO: Refactor to share these with runIde. + systemProperty("cody-agent.trace-path", "$buildDir/sourcegraph/cody-agent-trace.json") + systemProperty("cody-agent.directory", buildCodyDir.parent) + systemProperty("sourcegraph.verbose-logging", "true") + systemProperty( + "cody.autocomplete.enableFormatting", + project.property("cody.autocomplete.enableFormatting") as String? ?: "true") + + include { it.file.hasParentNamed("integrationTest") } + + useJUnit() + + systemProperty("cody.integration.testing", "true") + systemProperty( + "idea.test.execution.policy", // For now, should be used by all tests + "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") + + environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") + environment("CODY_RECORDING_MODE", "replay") + environment("CODY_RECORDING_DIRECTORY", "recordings") + environment("CODY_RECORD_IF_MISSING", "false") // Polly needs this to record at all. + + // ****************** TODO DO NOT COMMIT THIS *********************** + environment( + "CODY_INTEGRATION_TEST_TOKEN", + "sgp_a0d7ccb4f752ea73_822109f941a35a4e0c34a5dbccf946ffe7284671") + // ****************** TODO DO NOT COMMIT THIS *********************** + + dependsOn("buildCody") } // Make sure to set CODY_INTEGRATION_TEST_TOKEN when using this task. @@ -502,5 +543,19 @@ tasks { environment("CODY_RECORD_IF_MISSING", "true") // Polly needs this to record at all. } + named("processIntegrationTestResources") { + from(sourceSets["integrationTest"].resources) + into("$buildDir/resources/integrationTest") + exclude("**/.idea/**") + exclude("**/*.xml") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + + named("integrationTest") { dependsOn("processIntegrationTestResources") } + named("classpathIndexCleanup") { dependsOn("processIntegrationTestResources") } + + + withType { systemProperty("idea.test.src.dir", "$buildDir/resources/integrationTest") } + named("check") { dependsOn("integrationTest") } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 62912a94f..a7e2f6400 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -14,14 +15,19 @@ import com.sourcegraph.cody.edit.widget.LensAction import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner +import org.mockito.Mockito.mock +import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException import java.util.regex.Pattern -import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) + // Our test resources are copied into this directory, typically an in-memory filesystem. + private val moduleRootPath = ModuleRootManager.getInstance(module).contentRoots[0].path + override fun setUp() { super.setUp() configureFixture() @@ -43,7 +49,13 @@ class DocumentCodeTest : BasePlatformTestCase() { val foldingRangeFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES) executeDocumentCodeAction() runInEdtAndWait { - val rangeContext = foldingRangeFuture.get() + val rangeContext = + try { + foldingRangeFuture.get(5, TimeUnit.SECONDS) + } catch (t: TimeoutException) { + fail("Timed out waiting for folding ranges") + null + } assertNotNull(rangeContext) // Ensure we were able to get the selection range. val selection = rangeContext!!.session.selectionRange @@ -53,8 +65,7 @@ class DocumentCodeTest : BasePlatformTestCase() { assertFalse("Selection range should not be zero-width", selection!!.start == selection.end) // A more robust check is to see if the selection "range" is just the caret position. // If so, then our fallback range somehow made the round trip, which is bad. The lenses will - // go - // in the wrong places, etc. + // go in the wrong places, etc. val document = myFixture.editor.document val startOffset = selection.start.toOffset(document) val endOffset = selection.end.toOffset(document) @@ -65,7 +76,7 @@ class DocumentCodeTest : BasePlatformTestCase() { } } - fun testGetsWorkingGroupLens() { + fun skip_testGetsWorkingGroupLens() { val future = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_DISPLAY_WORKING_GROUP) executeDocumentCodeAction() @@ -105,7 +116,7 @@ class DocumentCodeTest : BasePlatformTestCase() { return context!! } - fun testShowsAcceptLens() { + fun skip_testShowsAcceptLens() { val context = awaitAcceptLensGroup() // Lens group should match the expected structure. @@ -143,7 +154,7 @@ class DocumentCodeTest : BasePlatformTestCase() { runInEdtAndWait { Disposer.dispose(lenses) } } - fun testAccept() { + fun skip_testAccept() { val project = myFixture.project!! assertNull(FixupService.getInstance(project).getActiveSession()) @@ -161,7 +172,7 @@ class DocumentCodeTest : BasePlatformTestCase() { assertNull(FixupService.getInstance(project).getActiveSession()) } - fun testUndo() { + fun skip_testUndo() { val undoFuture = subscribeToTopic(CodyInlineEditActionNotifier.TOPIC_PERFORM_UNDO) executeDocumentCodeAction() // The Accept/Retry/Undo group is now showing. @@ -178,28 +189,10 @@ class DocumentCodeTest : BasePlatformTestCase() { } private fun configureFixture() { - // spotless:off - myFixture.configureByText( - "Foo.java", - """ -import java.util.*; - -public class Foo { - - public void foo() { - List mystery = new ArrayList<>(); - mystery.add(0); - mystery.add(1); - for (int i = 2; i < 10; i++) { - mystery.add(mystery.get(i - 1) + mystery.get(i - 2)); - } - - for (int i = 0; i < 10; i++) { - System.out.println(mystery.get(i)); - } - } -} -""") // spotless:on + val env = System.getenv() + myFixture.testDataPath = + Paths.get(moduleRootPath, "build", "src/integrationTest/resources").toString() + myFixture.configureByFile("test-projects/document-code/src/main/java/Foo.java") } private fun subscribeToTopic( From 4dd4bf5e5d42e979b0980ee127bd0b461b4afe37 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 20:26:44 -0700 Subject: [PATCH 42/58] removed unused submodule declaration for jetbrains-shared --- settings.gradle.kts | 4 ---- .../kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index ee9352d8b..3722f0dce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,9 +1,5 @@ rootProject.name = "Sourcegraph" -include(":jetbrains-shared") - -project(":jetbrains-shared").projectDir = file("../jetbrains-shared") - val isCiServer = System.getenv().containsKey("CI") buildCache { local { isEnabled = !isCiServer } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index a7e2f6400..d83c68d4d 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -189,9 +189,9 @@ class DocumentCodeTest : BasePlatformTestCase() { } private fun configureFixture() { - val env = System.getenv() + // TODO: Copy our test project sources into this path. myFixture.testDataPath = - Paths.get(moduleRootPath, "build", "src/integrationTest/resources").toString() + Paths.get(moduleRootPath, "integrationTest/resources").toString() myFixture.configureByFile("test-projects/document-code/src/main/java/Foo.java") } From e0c1bd11e68707361ab76dd063bb7c6748c6ab2e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 21:25:41 -0700 Subject: [PATCH 43/58] more work on integration tests --- build.gradle.kts | 9 +++---- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 25 ++++++++++++------- .../test-projects/document-code/.gitignore | 1 - .../document-code/src/main/java/Foo.java | 17 ------------- .../cody/edit/widget/LensWidgetGroup.kt | 2 -- 5 files changed, 19 insertions(+), 35 deletions(-) delete mode 100644 src/integrationTest/resources/test-projects/document-code/.gitignore delete mode 100644 src/integrationTest/resources/test-projects/document-code/src/main/java/Foo.java diff --git a/build.gradle.kts b/build.gradle.kts index 62899ac92..57ebcafd9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -495,6 +495,9 @@ tasks { testClassesDirs = sourceSets["integrationTest"].output.classesDirs classpath = sourceSets["integrationTest"].runtimeClasspath + val resourcesPath = project.file("src/integrationTest/resources").absolutePath + systemProperty("test.resources.dir", resourcesPath) + // TODO: Refactor to share these with runIde. systemProperty("cody-agent.trace-path", "$buildDir/sourcegraph/cody-agent-trace.json") systemProperty("cody-agent.directory", buildCodyDir.parent) @@ -517,12 +520,6 @@ tasks { environment("CODY_RECORDING_DIRECTORY", "recordings") environment("CODY_RECORD_IF_MISSING", "false") // Polly needs this to record at all. - // ****************** TODO DO NOT COMMIT THIS *********************** - environment( - "CODY_INTEGRATION_TEST_TOKEN", - "sgp_a0d7ccb4f752ea73_822109f941a35a4e0c34a5dbccf946ffe7284671") - // ****************** TODO DO NOT COMMIT THIS *********************** - dependsOn("buildCody") } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index d83c68d4d..06177bd3d 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -4,7 +4,6 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -16,7 +15,8 @@ import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import org.mockito.Mockito.mock -import java.nio.file.Paths +import java.io.File +import java.nio.file.Files import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @@ -25,8 +25,7 @@ import java.util.regex.Pattern class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) - // Our test resources are copied into this directory, typically an in-memory filesystem. - private val moduleRootPath = ModuleRootManager.getInstance(module).contentRoots[0].path + private lateinit var testResourcesDir: File override fun setUp() { super.setUp() @@ -42,6 +41,9 @@ class DocumentCodeTest : BasePlatformTestCase() { } } // TODO: Notify the Agent that all documents were closed. + // -or, verify that CodyFileEditorListener works, and close them ourselves. + + testResourcesDir.deleteRecursively() super.tearDown() } @@ -51,7 +53,7 @@ class DocumentCodeTest : BasePlatformTestCase() { runInEdtAndWait { val rangeContext = try { - foldingRangeFuture.get(5, TimeUnit.SECONDS) + foldingRangeFuture.get(15, TimeUnit.SECONDS) } catch (t: TimeoutException) { fail("Timed out waiting for folding ranges") null @@ -189,10 +191,15 @@ class DocumentCodeTest : BasePlatformTestCase() { } private fun configureFixture() { - // TODO: Copy our test project sources into this path. - myFixture.testDataPath = - Paths.get(moduleRootPath, "integrationTest/resources").toString() - myFixture.configureByFile("test-projects/document-code/src/main/java/Foo.java") + // This is wherever src/integrationTest/resources is on the box running the tests. + testResourcesDir = File(System.getProperty("test.resources.dir")) + assertTrue(testResourcesDir.exists()) + + val testDataPath = Files.createTempDirectory("testData") + testResourcesDir.copyRecursively(testDataPath.toFile(), overwrite = true) + + myFixture.testDataPath = testDataPath.toString() + myFixture.configureByFile("testProjects/documentCode/src/main/java/Foo.java") } private fun subscribeToTopic( diff --git a/src/integrationTest/resources/test-projects/document-code/.gitignore b/src/integrationTest/resources/test-projects/document-code/.gitignore deleted file mode 100644 index 1cc4572e1..000000000 --- a/src/integrationTest/resources/test-projects/document-code/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.idea/workspace.xml diff --git a/src/integrationTest/resources/test-projects/document-code/src/main/java/Foo.java b/src/integrationTest/resources/test-projects/document-code/src/main/java/Foo.java deleted file mode 100644 index dbcf323ca..000000000 --- a/src/integrationTest/resources/test-projects/document-code/src/main/java/Foo.java +++ /dev/null @@ -1,17 +0,0 @@ -import java.util.*; - -public class Foo { - - public void foo() { - List mystery = new ArrayList<>(); - mystery.add(0); - mystery.add(1); - for (int i = 2; i < 10; i++) { - mystery.add(mystery.get(i - 1) + mystery.get(i - 2)); - } - - for (int i = 0; i < 10; i++) { - System.out.println(mystery.get(i)); - } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index 5954398b4..82a81b5a5 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -102,8 +102,6 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : editor.addEditorMouseListener(mouseClickListener) editor.addEditorMouseMotionListener(mouseMotionListener) addedListeners.set(true) - } else { - logger.warn("Disposing lens widget group with disposed session: $this") } } From da9dbb9dd005b93a4051554b5c5f16726d5bc31d Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 22:19:00 -0700 Subject: [PATCH 44/58] some refactoring of the integration test code moved the resource files into a testProjects directory --- build.gradle.kts | 59 +++++++++++++++++-- .../documentCode/src/main/java/.gitignore | 1 + .../documentCode/src/main/java/Foo.java | 17 ++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/integrationTest/resources/testProjects/documentCode/src/main/java/.gitignore create mode 100644 src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java diff --git a/build.gradle.kts b/build.gradle.kts index 57ebcafd9..7572bf643 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -458,8 +458,28 @@ tasks { } } + val integrationTestSystemProps = + mapOf( + "cody-agent.trace-path" to "$buildDir/sourcegraph/cody-agent-trace.json", + "cody-agent.directory" to buildCodyDir.parent, + "sourcegraph.verbose-logging" to "true", + "cody.autocomplete.enableFormatting" to + (project.property("cody.autocomplete.enableFormatting") as String? ?: "true"), + "cody.integration.testing" to "true", + // For now, should be used by all tests + "idea.test.execution.policy" to "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy", + "test.resources.dir" to project.file("src/integrationTest/resources").absolutePath) + + val integrationTestEnvVars = + mapOf( + "CODY_JETBRAINS_FEATURES" to "cody.feature.inline-edits=true", + "CODY_RECORDING_MODE" to "replay", + "CODY_RECORDING_DIRECTORY" to "recordings", + "CODY_RECORD_IF_MISSING" to "false") // Polly needs this to record at all. + // Common configuration for integration tests. - val sharedIntegrationTestConfig: Test.() -> Unit = { + // TODO: This doesn't actually work. + fun Test.sharedIntegrationTestConfig() { group = "verification" testClassesDirs = sourceSets["integrationTest"].output.classesDirs classpath = sourceSets["integrationTest"].runtimeClasspath @@ -474,6 +494,34 @@ tasks { include { it.file.hasParentNamed("integrationTest") } + integrationTestSystemProps.forEach { (k, v) -> systemProperty(k, v) } + integrationTestEnvVars.forEach { (k, v) -> environment(k, v) } + + useJUnit() + + dependsOn("buildCody") + } + + // Make sure to set CODY_INTEGRATION_TEST_TOKEN env var when using this task. + register("integrationTest") { + description = "Runs the integration tests." + group = "verification" + testClassesDirs = sourceSets["integrationTest"].output.classesDirs + classpath = sourceSets["integrationTest"].runtimeClasspath + + val resourcesPath = project.file("src/integrationTest/resources").absolutePath + systemProperty("test.resources.dir", resourcesPath) + + // TODO: Refactor to share these with runIde. + systemProperty("cody-agent.trace-path", "$buildDir/sourcegraph/cody-agent-trace.json") + systemProperty("cody-agent.directory", buildCodyDir.parent) + systemProperty("sourcegraph.verbose-logging", "true") + systemProperty( + "cody.autocomplete.enableFormatting", + project.property("cody.autocomplete.enableFormatting") as String? ?: "true") + + include { it.file.hasParentNamed("integrationTest") } + useJUnit() systemProperty("cody.integration.testing", "true") @@ -548,11 +596,14 @@ tasks { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } - named("integrationTest") { dependsOn("processIntegrationTestResources") } - named("classpathIndexCleanup") { dependsOn("processIntegrationTestResources") } + withType { systemProperty("idea.test.src.dir", "$buildDir/resources/integrationTest") } + named("integrationTest") { + dependsOn("processIntegrationTestResources") + //sharedIntegrationTestConfig() + } - withType { systemProperty("idea.test.src.dir", "$buildDir/resources/integrationTest") } + named("classpathIndexCleanup") { dependsOn("processIntegrationTestResources") } named("check") { dependsOn("integrationTest") } } diff --git a/src/integrationTest/resources/testProjects/documentCode/src/main/java/.gitignore b/src/integrationTest/resources/testProjects/documentCode/src/main/java/.gitignore new file mode 100644 index 000000000..bff2d7629 --- /dev/null +++ b/src/integrationTest/resources/testProjects/documentCode/src/main/java/.gitignore @@ -0,0 +1 @@ +*.iml diff --git a/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java b/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java new file mode 100644 index 000000000..dbcf323ca --- /dev/null +++ b/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java @@ -0,0 +1,17 @@ +import java.util.*; + +public class Foo { + + public void foo() { + List mystery = new ArrayList<>(); + mystery.add(0); + mystery.add(1); + for (int i = 2; i < 10; i++) { + mystery.add(mystery.get(i - 1) + mystery.get(i - 2)); + } + + for (int i = 0; i < 10; i++) { + System.out.println(mystery.get(i)); + } + } +} From 374dfecdde5237c763ac9d9b78cba3610141b2a5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 22:33:16 -0700 Subject: [PATCH 45/58] removed accidental redundant declaration --- build.gradle.kts | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7572bf643..2da336f2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -537,40 +537,6 @@ tasks { dependsOn("buildCody") } - register("integrationTest") { - description = "Runs the integration tests." - group = "verification" - testClassesDirs = sourceSets["integrationTest"].output.classesDirs - classpath = sourceSets["integrationTest"].runtimeClasspath - - val resourcesPath = project.file("src/integrationTest/resources").absolutePath - systemProperty("test.resources.dir", resourcesPath) - - // TODO: Refactor to share these with runIde. - systemProperty("cody-agent.trace-path", "$buildDir/sourcegraph/cody-agent-trace.json") - systemProperty("cody-agent.directory", buildCodyDir.parent) - systemProperty("sourcegraph.verbose-logging", "true") - systemProperty( - "cody.autocomplete.enableFormatting", - project.property("cody.autocomplete.enableFormatting") as String? ?: "true") - - include { it.file.hasParentNamed("integrationTest") } - - useJUnit() - - systemProperty("cody.integration.testing", "true") - systemProperty( - "idea.test.execution.policy", // For now, should be used by all tests - "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") - - environment("CODY_JETBRAINS_FEATURES", "cody.feature.inline-edits=true") - environment("CODY_RECORDING_MODE", "replay") - environment("CODY_RECORDING_DIRECTORY", "recordings") - environment("CODY_RECORD_IF_MISSING", "false") // Polly needs this to record at all. - - dependsOn("buildCody") - } - // Make sure to set CODY_INTEGRATION_TEST_TOKEN when using this task. register("passthroughIntegrationTest") { description = "Runs the integration tests, passing everything through to the LLM." From 926dc2b11685efbed0d00d283f51afc1224faebb Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 22:33:39 -0700 Subject: [PATCH 46/58] added some assertions --- .../kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 06177bd3d..14cc65075 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -17,10 +17,12 @@ import com.sourcegraph.cody.edit.widget.LensSpinner import org.mockito.Mockito.mock import java.io.File import java.nio.file.Files +import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.regex.Pattern +import kotlin.io.path.pathString class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) @@ -199,7 +201,11 @@ class DocumentCodeTest : BasePlatformTestCase() { testResourcesDir.copyRecursively(testDataPath.toFile(), overwrite = true) myFixture.testDataPath = testDataPath.toString() - myFixture.configureByFile("testProjects/documentCode/src/main/java/Foo.java") + val sourcePath = + Paths.get(testDataPath.pathString, "testProjects/documentCode/src/main/java/Foo.java") + .toString() + assertTrue(File(sourcePath).exists()) + myFixture.configureByFile(sourcePath) } private fun subscribeToTopic( From 64a501c1bb6b8731157e285c62b0e387651d601b Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 22:36:42 -0700 Subject: [PATCH 47/58] moved .gitignore back to proper spot --- .../testProjects/documentCode/{src/main/java => }/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/integrationTest/resources/testProjects/documentCode/{src/main/java => }/.gitignore (100%) diff --git a/src/integrationTest/resources/testProjects/documentCode/src/main/java/.gitignore b/src/integrationTest/resources/testProjects/documentCode/.gitignore similarity index 100% rename from src/integrationTest/resources/testProjects/documentCode/src/main/java/.gitignore rename to src/integrationTest/resources/testProjects/documentCode/.gitignore From 764dbf90b0191079d02b29171569c0f6bb9d488e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 May 2024 22:41:53 -0700 Subject: [PATCH 48/58] fixed problem with accidentally deleting our own source --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 14cc65075..8ad40264d 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -22,12 +22,11 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.regex.Pattern -import kotlin.io.path.pathString class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) - private lateinit var testResourcesDir: File + private lateinit var testDataPath: File override fun setUp() { super.setUp() @@ -45,7 +44,7 @@ class DocumentCodeTest : BasePlatformTestCase() { // TODO: Notify the Agent that all documents were closed. // -or, verify that CodyFileEditorListener works, and close them ourselves. - testResourcesDir.deleteRecursively() + testDataPath.deleteRecursively() super.tearDown() } @@ -194,18 +193,18 @@ class DocumentCodeTest : BasePlatformTestCase() { private fun configureFixture() { // This is wherever src/integrationTest/resources is on the box running the tests. - testResourcesDir = File(System.getProperty("test.resources.dir")) + val testResourcesDir = File(System.getProperty("test.resources.dir")) assertTrue(testResourcesDir.exists()) - val testDataPath = Files.createTempDirectory("testData") - testResourcesDir.copyRecursively(testDataPath.toFile(), overwrite = true) + testDataPath = Files.createTempDirectory("testData").toFile() + testResourcesDir.copyRecursively(testDataPath, overwrite = true) myFixture.testDataPath = testDataPath.toString() - val sourcePath = - Paths.get(testDataPath.pathString, "testProjects/documentCode/src/main/java/Foo.java") - .toString() + // The file we pass to configureByFile must be relative to testDataPath. + val projectFile = "testProjects/documentCode/src/main/java/Foo.java" + val sourcePath = Paths.get(testDataPath.path, projectFile).toString() assertTrue(File(sourcePath).exists()) - myFixture.configureByFile(sourcePath) + myFixture.configureByFile(projectFile) } private fun subscribeToTopic( From e763e3430f8cba1534f3ef09cfd41cc03a7ffaa3 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 16 May 2024 16:03:01 -0700 Subject: [PATCH 49/58] Finished configuring integration tests from sources on disk --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 45 +++++++++++++------ .../agent/protocol/ProtocolTextDocument.kt | 11 ++++- .../cody/edit/sessions/FixupSession.kt | 1 - 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 8ad40264d..d630957a4 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -14,9 +15,9 @@ import com.sourcegraph.cody.edit.widget.LensAction import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner +import com.sourcegraph.config.ConfigUtil import org.mockito.Mockito.mock import java.io.File -import java.nio.file.Files import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @@ -34,18 +35,28 @@ class DocumentCodeTest : BasePlatformTestCase() { } override fun tearDown() { - FixupService.getInstance(myFixture.project).getActiveSession()?.apply { + try { + FixupService.getInstance(myFixture.project).getActiveSession()?.apply { + try { + finish() + } catch (x: Exception) { + logger.warn("Error shutting down session", x) + } + } + // Notify the Agent that all documents have been closed. + val fileEditorManager = FileEditorManager.getInstance(myFixture.project) + fileEditorManager.openFiles.forEach { + // TODO: Check that this shows up in the trace.json file (textDocument/didClose). + fileEditorManager.closeFile(it) + } try { - finish() + testDataPath.deleteRecursively() } catch (x: Exception) { - logger.warn("Error shutting down session", x) + logger.warn("Error deleting test data", x) } + } finally { + super.tearDown() } - // TODO: Notify the Agent that all documents were closed. - // -or, verify that CodyFileEditorListener works, and close them ourselves. - - testDataPath.deleteRecursively() - super.tearDown() } fun testGetsFoldingRanges() { @@ -54,12 +65,12 @@ class DocumentCodeTest : BasePlatformTestCase() { runInEdtAndWait { val rangeContext = try { - foldingRangeFuture.get(15, TimeUnit.SECONDS) + foldingRangeFuture.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) } catch (t: TimeoutException) { fail("Timed out waiting for folding ranges") null } - assertNotNull(rangeContext) + assertNotNull("Computed selection range should be non-null", rangeContext) // Ensure we were able to get the selection range. val selection = rangeContext!!.session.selectionRange assertNotNull("Selection should have been set", selection) @@ -196,10 +207,14 @@ class DocumentCodeTest : BasePlatformTestCase() { val testResourcesDir = File(System.getProperty("test.resources.dir")) assertTrue(testResourcesDir.exists()) - testDataPath = Files.createTempDirectory("testData").toFile() + // During test runs this is set by IntelliJ to a private temp folder. + // We pass it to the Agent during initialization. + val workspaceRootUri = ConfigUtil.getWorkspaceRootPath(project) + + testDataPath = Paths.get(workspaceRootUri.toString(), "src/").toFile() testResourcesDir.copyRecursively(testDataPath, overwrite = true) - myFixture.testDataPath = testDataPath.toString() + myFixture.testDataPath = testDataPath.path // The file we pass to configureByFile must be relative to testDataPath. val projectFile = "testProjects/documentCode/src/main/java/Foo.java" val sourcePath = Paths.get(testDataPath.path, projectFile).toString() @@ -225,6 +240,7 @@ class DocumentCodeTest : BasePlatformTestCase() { return future } + // Run the IDE action specified by actionId. private fun triggerAction(actionId: String) { val action = ActionManager.getInstance().getAction(actionId) action.actionPerformed( @@ -247,7 +263,8 @@ class DocumentCodeTest : BasePlatformTestCase() { // TODO: find the lowest value this can be for production, and use it // If it's too low the test may be flaky. - // const val ASYNC_WAIT_TIMEOUT_SECONDS = 10000L //debug + //const val ASYNC_WAIT_TIMEOUT_SECONDS = 15000L + const val ASYNC_WAIT_TIMEOUT_SECONDS = 15L } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt index b4359c302..e0e3d3387 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.vfs.VirtualFile import com.sourcegraph.cody.agent.protocol.util.Rfc3986UriEncoder +import java.net.URI class ProtocolTextDocument private constructor( @@ -47,8 +48,14 @@ private constructor( return ProtocolTextDocument(uriFor(file), text, selection) } - fun uriFor(file: VirtualFile): String { - return Rfc3986UriEncoder.encode(file.url) + private fun uriFor(file: VirtualFile): String { + // Convert integration-test temp:// to file:// for Agent. + val initialUri = URI(file.url) + val path = initialUri.path + // Don't let it produce syntactically incorrect "file:/src/foo/bar" URIs. + // This construction forces it to have the syntax "file:///src/foo/bar". + val properUri = URI("file", "", path, null).toString() + return Rfc3986UriEncoder.encode(properUri) } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index ad5b3f3ab..e7c28a39a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -309,7 +309,6 @@ abstract class FixupSession( op.uri?.let { updateEditorIfNeeded(it) } - // TODO: We need to support the file-level operations. when (op.type) { "create-file" -> { logger.warn("Workspace edit operation created a file: ${op.uri}") From 14d07b9677e4853bc0221283e2ba7341bdcb76f4 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 16 May 2024 23:06:59 -0700 Subject: [PATCH 50/58] added support for setting caret and selection range in test source files --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 55 ++++++++++++++++++- .../documentCode/src/main/java/Foo.java | 2 +- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index d630957a4..0c8ec89cd 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -3,7 +3,10 @@ package com.sourcegraph.cody.edit import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.util.Disposer import com.intellij.testFramework.EditorTestUtil @@ -50,7 +53,8 @@ class DocumentCodeTest : BasePlatformTestCase() { fileEditorManager.closeFile(it) } try { - testDataPath.deleteRecursively() + // TODO: This seemed to kill one of the tests. + //testDataPath.deleteRecursively() } catch (x: Exception) { logger.warn("Error deleting test data", x) } @@ -211,15 +215,64 @@ class DocumentCodeTest : BasePlatformTestCase() { // We pass it to the Agent during initialization. val workspaceRootUri = ConfigUtil.getWorkspaceRootPath(project) + // We copy the test resources there manually, bypassing Gradle, which is picky. testDataPath = Paths.get(workspaceRootUri.toString(), "src/").toFile() testResourcesDir.copyRecursively(testDataPath, overwrite = true) + // This useful setting lets us tell the fixture to look where we copied them. myFixture.testDataPath = testDataPath.path + // The file we pass to configureByFile must be relative to testDataPath. val projectFile = "testProjects/documentCode/src/main/java/Foo.java" val sourcePath = Paths.get(testDataPath.path, projectFile).toString() assertTrue(File(sourcePath).exists()) myFixture.configureByFile(projectFile) + + initCaretPosition() + } + + // This provides a crude mechanism for specifying the caret position in the test file. + private fun initCaretPosition() { + runInEdtAndWait { + val virtualFile = myFixture.file.virtualFile + val document = FileDocumentManager.getInstance().getDocument(virtualFile)!! + val caretToken = "[[caret]]" + val caretIndex = document.text.indexOf(caretToken) + + if (caretIndex != -1) { // Remove caret token from doc + WriteCommandAction.runWriteCommandAction(project) { + document.deleteString(caretIndex, caretIndex + caretToken.length) + } + // Place the caret at the position where the token was found. + myFixture.editor.caretModel.moveToOffset(caretIndex) + //myFixture.editor.selectionModel.setSelection(caretIndex, caretIndex) + } else { + initSelectionRange() + } + } + } + + // Provides a mechanism to specify the selection range via [[start]] and [[end]]. + // The tokens are removed and the range is selected, notifying the Agent. + private fun initSelectionRange() { + runInEdtAndWait { + val virtualFile = myFixture.file.virtualFile + val document = FileDocumentManager.getInstance().getDocument(virtualFile)!! + val startToken = "[[start]]" + val endToken = "[[end]]" + val start = document.text.indexOf(startToken) + val end = document.text.indexOf(endToken) + // Remove the tokens from the document. + if (start != -1 && end != -1) { + ApplicationManager.getApplication().runWriteAction { + document.deleteString(start, start + startToken.length) + document.deleteString(end, end + endToken.length) + } + myFixture.editor.selectionModel.setSelection(start, end) + } else { + logger.warn("No caret or selection range specified in test file.") + } + } } private fun subscribeToTopic( diff --git a/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java b/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java index dbcf323ca..71e4baf07 100644 --- a/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java +++ b/src/integrationTest/resources/testProjects/documentCode/src/main/java/Foo.java @@ -6,7 +6,7 @@ public void foo() { List mystery = new ArrayList<>(); mystery.add(0); mystery.add(1); - for (int i = 2; i < 10; i++) { + [[caret]]for (int i = 2; i < 10; i++) { mystery.add(mystery.get(i - 1) + mystery.get(i - 2)); } From 7eeb1cd616fb79d996c92f1159ff569b44270ae0 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 18 May 2024 15:14:56 -0700 Subject: [PATCH 51/58] fixed a compile error --- build.gradle.kts | 9 ++++----- .../com/sourcegraph/cody/edit/DocumentCodeTest.kt | 10 +++++----- .../sourcegraph/cody/edit/widget/LensWidgetGroup.kt | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2da336f2e..ef1e40bfe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,4 @@ - import com.jetbrains.plugin.structure.base.utils.isDirectory -import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL import java.nio.file.FileSystems import java.nio.file.FileVisitResult @@ -16,6 +12,9 @@ import java.util.* import java.util.jar.JarFile import java.util.zip.ZipFile import kotlin.script.experimental.jvm.util.hasParentNamed +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String) = project.findProperty(key).toString() @@ -566,7 +565,7 @@ tasks { named("integrationTest") { dependsOn("processIntegrationTestResources") - //sharedIntegrationTestConfig() + // sharedIntegrationTestConfig() } named("classpathIndexCleanup") { dependsOn("processIntegrationTestResources") } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 0c8ec89cd..c6121f088 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -19,13 +19,13 @@ import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import com.sourcegraph.config.ConfigUtil -import org.mockito.Mockito.mock import java.io.File import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.regex.Pattern +import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) @@ -41,7 +41,7 @@ class DocumentCodeTest : BasePlatformTestCase() { try { FixupService.getInstance(myFixture.project).getActiveSession()?.apply { try { - finish() + dismiss() } catch (x: Exception) { logger.warn("Error shutting down session", x) } @@ -54,7 +54,7 @@ class DocumentCodeTest : BasePlatformTestCase() { } try { // TODO: This seemed to kill one of the tests. - //testDataPath.deleteRecursively() + // testDataPath.deleteRecursively() } catch (x: Exception) { logger.warn("Error deleting test data", x) } @@ -245,7 +245,7 @@ class DocumentCodeTest : BasePlatformTestCase() { } // Place the caret at the position where the token was found. myFixture.editor.caretModel.moveToOffset(caretIndex) - //myFixture.editor.selectionModel.setSelection(caretIndex, caretIndex) + // myFixture.editor.selectionModel.setSelection(caretIndex, caretIndex) } else { initSelectionRange() } @@ -316,7 +316,7 @@ class DocumentCodeTest : BasePlatformTestCase() { // TODO: find the lowest value this can be for production, and use it // If it's too low the test may be flaky. - //const val ASYNC_WAIT_TIMEOUT_SECONDS = 15000L + // const val ASYNC_WAIT_TIMEOUT_SECONDS = 15000L const val ASYNC_WAIT_TIMEOUT_SECONDS = 15L } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt index ddf336fd2..9417d02fc 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -22,7 +22,6 @@ import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.EditCommandPrompt import com.sourcegraph.cody.edit.sessions.FixupSession import com.sourcegraph.config.ThemeUtil -import org.jetbrains.annotations.NotNull import java.awt.Cursor import java.awt.Font import java.awt.FontMetrics @@ -33,6 +32,7 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Supplier import kotlin.math.roundToInt +import org.jetbrains.annotations.NotNull operator fun Point.component1() = this.x From 3f45fc11b4f9e3b456d74a44a1aa8b31d5be66f5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 18 May 2024 16:00:15 -0700 Subject: [PATCH 52/58] fixed a bug where performing the inline edit was canceling the task --- .../cody/edit/sessions/FixupSession.kt | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index e20215ee1..901dfd694 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -66,6 +66,7 @@ abstract class FixupSession( private set private val showedAcceptLens = AtomicBoolean(false) + private val isInlineEditInProgress = AtomicBoolean(false) val isDisposed = AtomicBoolean(false) var selectionRange: Range? = null @@ -173,7 +174,7 @@ abstract class FixupSession( fun update(task: EditTask) { task.instruction?.let { instruction = it } - + logger.warn("New task: $task state=${task.state}") when (task.state) { // This is an internal state (parked/ready tasks) and we should never see it. CodyTaskState.Idle -> {} @@ -258,8 +259,11 @@ abstract class FixupSession( showErrorGroup(text, hoverText ?: "No additional info from Agent") } + // TODO: Check how we accept/cancel on origin/main + fun handleDocumentChange(editorThatChanged: Editor) { if (editorThatChanged != editor) return + if (isInlineEditInProgress.get()) return // We auto-accept if they edit the document after we've put up this lens group. if (showedAcceptLens.get()) { accept() @@ -365,44 +369,51 @@ abstract class FixupSession( } fun performInlineEdits(edits: List) { - // TODO: This is an artifact of the update to concurrent editing tasks. - // We do need to mute any LensGroup listeners, but this is an ugly way to do it. - // There are multiple Lens groups; we need a Document-level listener list. - lensGroup?.withListenersMuted { - if (!controller.isEligibleForInlineEdit(editor)) { - return@withListenersMuted logger.warn("Inline edit not eligible") + isInlineEditInProgress.set(true) + try { + // TODO: This is ugly; find a better way to mute the listeners. + lensGroup?.withListenersMuted { + if (!controller.isEligibleForInlineEdit(editor)) { + logger.warn("Inline edit not eligible") + return@withListenersMuted + } + + performInlineEditActions(edits) } + } finally { + isInlineEditInProgress.set(false) + publishProgress(CodyInlineEditActionNotifier.TOPIC_TEXT_DOCUMENT_EDIT) + } + } - WriteCommandAction.runWriteCommandAction(project) { - val currentActions = - edits.mapNotNull { edit -> - try { - when (edit.type) { - "replace", - "delete" -> ReplaceUndoableAction(project, edit, document) - "insert" -> InsertUndoableAction(project, edit, document) - else -> { - logger.warn("Unknown edit type: ${edit.type}") - null - } + private fun performInlineEditActions(edits: List) { + WriteCommandAction.runWriteCommandAction(project) { + val currentActions = + edits.mapNotNull { edit -> + try { + when (edit.type) { + "replace", + "delete" -> ReplaceUndoableAction(project, edit, document) + "insert" -> InsertUndoableAction(project, edit, document) + else -> { + logger.warn("Unknown edit type: ${edit.type}") + null } - } catch (e: RuntimeException) { - throw EditCreationException(edit, e) } + } catch (e: RuntimeException) { + throw EditCreationException(edit, e) } - - currentActions.forEach { action -> - try { - action.apply() - } catch (e: RuntimeException) { - throw EditExecutionException(action, e) } - } - performedActions += currentActions + currentActions.forEach { action -> + try { + action.apply() + } catch (e: RuntimeException) { + throw EditExecutionException(action, e) + } } - publishProgress(CodyInlineEditActionNotifier.TOPIC_TEXT_DOCUMENT_EDIT) + performedActions += currentActions } } From c5c2744bd4b1ce548fc49ece505a6b3dad4ced10 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 18 May 2024 17:45:05 -0700 Subject: [PATCH 53/58] comments --- .../kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 901dfd694..c27f3608e 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -259,7 +259,7 @@ abstract class FixupSession( showErrorGroup(text, hoverText ?: "No additional info from Agent") } - // TODO: Check how we accept/cancel on origin/main + // TODO: Check how we accept/cancel on origin/main - is this still valid? fun handleDocumentChange(editorThatChanged: Editor) { if (editorThatChanged != editor) return From 4d61876fab5c0617d846ab48943d43e98e352a0e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 20 May 2024 11:42:11 -0700 Subject: [PATCH 54/58] more work on integration test pubsub, not quite working yet --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 6 +++--- .../cody/edit/CodyInlineEditActionNotifier.kt | 3 ++- .../com/sourcegraph/cody/edit/FixupService.kt | 8 ++++++++ .../cody/edit/sessions/FixupSession.kt | 19 ++++++++++++++----- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index c6121f088..9400163c0 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -19,13 +19,13 @@ import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import com.sourcegraph.config.ConfigUtil +import org.mockito.Mockito.mock import java.io.File import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.regex.Pattern -import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) @@ -69,14 +69,14 @@ class DocumentCodeTest : BasePlatformTestCase() { runInEdtAndWait { val rangeContext = try { - foldingRangeFuture.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + foldingRangeFuture.get() } catch (t: TimeoutException) { fail("Timed out waiting for folding ranges") null } assertNotNull("Computed selection range should be non-null", rangeContext) // Ensure we were able to get the selection range. - val selection = rangeContext!!.session.selectionRange + val selection = rangeContext!!.selectionRange assertNotNull("Selection should have been set", selection) // We set the selection range to whatever the protocol returns. // If a 0-width selection turns out to be reasonable we can adjust or remove this test. diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt index 4a89c96ab..bb0016f01 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodyInlineEditActionNotifier.kt @@ -1,13 +1,14 @@ package com.sourcegraph.cody.edit import com.intellij.util.messages.Topic +import com.sourcegraph.cody.agent.protocol.Range import com.sourcegraph.cody.edit.sessions.FixupSession /** Pubsub interface shared by all inline edit notifications that accept a FixupSession. */ interface CodyInlineEditActionNotifier { // Encapsulates the FixupSession and allows adding new fields without breaking subscribers. - data class Context(val session: FixupSession) + data class Context(val session: FixupSession, val selectionRange: Range? = null) fun afterAction(context: Context) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt index dd051ef8f..615659f5b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/FixupService.kt @@ -15,6 +15,7 @@ import com.sourcegraph.cody.edit.sessions.TestCodeSession import com.sourcegraph.cody.ignore.ActionInIgnoredFileNotification import com.sourcegraph.cody.ignore.IgnoreOracle import com.sourcegraph.cody.ignore.IgnorePolicy +import com.sourcegraph.config.ConfigUtil import com.sourcegraph.config.ConfigUtil.isCodyEnabled import com.sourcegraph.utils.CodyEditorUtil import java.util.concurrent.atomic.AtomicReference @@ -69,6 +70,13 @@ class FixupService(val project: Project) : Disposable { return false } val policy = IgnoreOracle.getInstance(project).policyForEditor(editor) + + // TODO: We'll have to figure out a way to integration-test the ignore stuff. + // But for now, the policy comes back null during testing, which would normally + // ignore it. + if (ConfigUtil.isIntegrationTestModeEnabled()) { + return true + } if (policy != IgnorePolicy.USE) { runInEdt { ActionInIgnoredFileNotification().notify(project) } logger.warn("Ignoring file for inline edits: $editor, policy=$policy") diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index c27f3608e..d392d4285 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -166,8 +166,10 @@ abstract class FixupSession( agent.server .getFoldingRanges(GetFoldingRangeParams(uri = textFile.uri, range = selection)) .thenApply { result -> - publishProgressOnEdt(CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES) selectionRange = result.range + publishProgressOnEdt( + CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES, + CodyInlineEditActionNotifier.Context(session = this, selectionRange = result.range)) } .get() } @@ -462,14 +464,21 @@ abstract class FixupSession( } } - private fun publishProgressOnEdt(topic: Topic) { + private fun publishProgressOnEdt( + topic: Topic, + context: CodyInlineEditActionNotifier.Context? = null + ) { ApplicationManager.getApplication().invokeLater { - project.messageBus - .syncPublisher(topic) - .afterAction(CodyInlineEditActionNotifier.Context(session = this)) + val progressContext = context ?: CodyInlineEditActionNotifier.Context(session = this) + project.messageBus.syncPublisher(topic).afterAction(progressContext) } } + override fun toString(): String { + val file = FileDocumentManager.getInstance().getFile(editor.document) + return "${this::javaClass.name} for ${file?.path ?: "unknown file"}" + } + companion object { // JetBrains Actions that we fire when the lenses are clicked. const val ACTION_ACCEPT = "cody.inlineEditAcceptAction" From fe0865f1d57e2e151eed64e4bae881ab6201161a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 27 May 2024 12:25:04 -0700 Subject: [PATCH 55/58] fixed another merge error --- .../kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index dbc12afa9..7a4ae45c2 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -1,7 +1,6 @@ package com.sourcegraph.cody.edit.sessions import FixupSessionDocumentListener -import com.intellij.codeInsight.codeVision.CodeVisionState.NotReady.result import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt @@ -174,9 +173,9 @@ abstract class FixupSession( publishProgressOnEdt( CodyInlineEditActionNotifier.TOPIC_FOLDING_RANGES, CodyInlineEditActionNotifier.Context(session = this, selectionRange = result.range)) + result } .get() - selectionRange = result.range.toRangeMarker(document, true) } catch (e: Exception) { logger.warn("Error getting folding range", e) } From 8636991eac2f7d9dc5f2f5094fe027ac1c3d910a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 27 May 2024 12:32:13 -0700 Subject: [PATCH 56/58] fixed more merge errors --- .../com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt | 2 +- .../kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt index 303ee75ed..cc9a09147 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt @@ -191,7 +191,7 @@ private constructor( } @JvmStatic - private fun uriFor(file: VirtualFile): String { + fun uriFor(file: VirtualFile): String { // Integration test: Convert default filesystem "temp://" scheme to "file://" for Agent. val initialUri = FileSystems.getDefault().getPath(file.path) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt index 7a4ae45c2..c6390f806 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -217,7 +217,7 @@ abstract class FixupSession( } catch (x: Exception) { logger.warn("Error disposing previous lens group", x) } - if (isDisposed.get()) return + if (isDisposed.get()) return@runInEdt lensGroup = group var range = From 1ebab918ccb93c306b843fd74d89727219a8ab85 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 27 May 2024 12:34:17 -0700 Subject: [PATCH 57/58] another merge error slipped through --- .../com/sourcegraph/cody/listeners/CodyDocumentListener.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt index 814f3b525..5ef5347e3 100644 --- a/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/listeners/CodyDocumentListener.kt @@ -13,7 +13,6 @@ import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument import com.sourcegraph.cody.autocomplete.CodyAutocompleteManager import com.sourcegraph.cody.autocomplete.action.AcceptCodyAutocompleteAction import com.sourcegraph.cody.chat.CodeEditorFactory -import com.sourcegraph.cody.edit.FixupService import com.sourcegraph.cody.vscode.InlineCompletionTriggerKind import com.sourcegraph.telemetry.GraphQlLogger @@ -74,7 +73,6 @@ class CodyDocumentListener(val project: Project) : BulkAwareDocumentListener { CodyAutocompleteManager.instance.triggerAutocomplete( editor, changeOffset, InlineCompletionTriggerKind.AUTOMATIC) } - FixupService.getInstance(project).getActiveSession()?.handleDocumentChange(editor) } } } From 791261424a8a15f5d8bdd8c5fafd292891136dd6 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 27 May 2024 12:36:54 -0700 Subject: [PATCH 58/58] fixed another post-merge bug --- .../kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt | 4 ++-- .../kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 05ee6ab5a..4b673e682 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -19,13 +19,13 @@ import com.sourcegraph.cody.edit.widget.LensGroupFactory import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner import com.sourcegraph.config.ConfigUtil +import org.mockito.Mockito.mock import java.io.File import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.regex.Pattern -import org.mockito.Mockito.mock class DocumentCodeTest : BasePlatformTestCase() { private val logger = Logger.getInstance(DocumentCodeTest::class.java) @@ -41,7 +41,7 @@ class DocumentCodeTest : BasePlatformTestCase() { try { FixupService.getInstance(myFixture.project).getActiveSession()?.apply { try { - dismiss() + dispose() } catch (x: Exception) { logger.warn("Error shutting down session", x) } diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt index 7de160eb1..4fb235e16 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt @@ -19,7 +19,7 @@ import java.awt.geom.Rectangle2D class LensAction( val group: LensWidgetGroup, private val text: String, - private val actionId: String + val actionId: String ) : LensWidget(group) { private val highlight =