diff --git a/.github/workflows/release.yml b/.github/workflows/nightly-release.yml similarity index 83% rename from .github/workflows/release.yml rename to .github/workflows/nightly-release.yml index 845e8ae34c..2dd468154b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/nightly-release.yml @@ -1,4 +1,4 @@ -name: Release to Marketplace +name: Nightly Release on: push: tags: [ "*" ] @@ -28,11 +28,7 @@ jobs: - run: | echo "RELEASE_VERSION=$(./scripts/version-from-git-tag.sh)" >> $GITHUB_ENV - run: echo "Publishing version $RELEASE_VERSION" - - run: ./gradlew "-PpluginVersion=$RELEASE_VERSION" publishPlugin - env: - PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - name: Publish nightly version - if: "!endsWith(env.RELEASE_VERSION, '-nightly')" run: | echo "Publishing nightly version ${RELEASE_VERSION}-nightly" ./gradlew "-PpluginVersion=${RELEASE_VERSION}-nightly" publishPlugin diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml new file mode 100644 index 0000000000..8821c0c3de --- /dev/null +++ b/.github/workflows/stable-release.yml @@ -0,0 +1,32 @@ +name: Stable Release +on: + workflow_dispatch +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: gradle + # See note about QEMU and binfmt requirement here https://github.com/vercel/pkg#targets + - name: Set up QEMU + id: qemu + uses: docker/setup-qemu-action@v3 + with: + image: tonistiigi/binfmt:latest + platforms: all + - name: Gradle Wrapper Validation + uses: gradle/wrapper-validation-action@v3 + - run: yarn global add pnpm@8.6.7 + - run: | + echo "RELEASE_VERSION=$(./scripts/version-from-git-tag.sh)" >> $GITHUB_ENV + - run: echo "Publishing version $RELEASE_VERSION" + - run: ./gradlew "-PpluginVersion=$RELEASE_VERSION" publishPlugin + env: + PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b4df98a41..ff96f20a65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: JetBrains Tests +name: Build, test and verify on: pull_request: push: @@ -46,18 +46,22 @@ jobs: - run: echo "SKIP_CODE_SEARCH_BUILD=true" >> $GITHUB_ENV - run: ./gradlew spotlessCheck - run: ./gradlew check - - name: Upload test report + - name: Upload the test report if: always() uses: actions/upload-artifact@v4 with: name: test-report path: build/reports/tests/ + compression-level: 9 - run: ./gradlew buildPlugin - run: ./gradlew --stop - - uses: actions/upload-artifact@v4 + - name: Upload the plugin package + uses: actions/upload-artifact@v4 with: - name: plugin.zip + name: plugin path: './build/distributions/Sourcegraph-*.zip' + compression-level: 0 + retention-days: 7 plugin-verifier: name: IntelliJ Plugin Verifier runs-on: ubuntu-latest @@ -66,7 +70,7 @@ jobs: steps: - uses: actions/download-artifact@v4 with: - name: plugin.zip + name: plugin - name: Verify Plugin on IntelliJ Platforms id: verify uses: ChrisCarini/intellij-platform-plugin-verifier-action@v2.0.1 @@ -85,8 +89,11 @@ jobs: PLUGIN_STRUCTURE_WARNINGS MISSING_DEPENDENCIES INVALID_PLUGIN - - uses: actions/upload-artifact@v4 + - name: Upload the verification reports + if: always() + uses: actions/upload-artifact@v4 with: name: plugin-verifier-reports path: 'verification-*' + compression-level: 9 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13133e8a73..6664bf147e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,28 +117,33 @@ After doing that: ## Publishing a New Release +### Historical context +We used to publish both stable and nightly channel versions at once. +In that approach QA testing and JB approval happened in parallel. +However, it consumed a lot of CI time and JB time for the releases that did not pass our QA +(and did not go public eventually). Hence, we decided to use a sequential process. +We trigger the stable channel release only after the nightly channel release passes QA. + ```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"]; + Title --> nightly["Nightly Release"]; + Title["JetBrains Plugin Release"] --> stable["Stable Release"]; + stable --> trigger_stable["Manually trigger 'Stable Release' workflow\nin GitHub Actions"]; + release_stable --> marketplace_approval["Wait for JetBrains 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\nto manually approve it via Slack"]; + slack_approval --> unhide["Unhide the approved release\n(requires admin access)"]; + nightly --> push_nightly["Run `push-git-tag-for-next-release.sh`"]; + trigger_stable --> release_stable["Wait for 'Stable Release' workflow to complete"]; + push_nightly --> release_nightly["Wait for 'Nightly Release' workflow to complete"]; + release_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 - -First, choose whether to publish a new version of nightly or stable. +### 1. Push a git tag & publish a nightly release Use the following command for a **patch** release: @@ -162,11 +167,14 @@ Or this one for a **major** release This script runs `verify-release.sh`, which takes a long time to run with a clean cache, which is why we don't run it in CI. When you have a local cache of IDEA installations then this script can run decently fast (~1-2min). -After successfully pushing the new tag (for example: `v5.2.4819` or `v5.2.4249-nightly`), we are now able to publish. +After successfully pushing the new tag (for example: `v6.0.15`), we are now able to publish. Wait for the `Release to Marketplace` GitHub workflow to complete. -### 2. For Stable releases, wait for Marketplace approval +### 2. Publish a stable release + +Go to [Stable Release workflow](https://github.com/sourcegraph/jetbrains/actions/workflows/stable-release.yml), +click `Run workflow` and select the tag that has been pushed before (and tested by QA team), run it. 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 diff --git a/README.md b/README.md index 751a4f5f2f..385a631545 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ Experience experimental chat and command support with Ollama running locally: 1. Install and run [Ollama](https://ollama.com/download). 2. Set the [OLLAMA_HOST](https://sourcegraph.com/github.com/ollama/ollama@main/-/blob/docs/faq.md#how-do-i-configure-ollama-server) to `0.0.0.0`. 1. Please refer to the [official Ollama docs](https://sourcegraph.com/github.com/ollama/ollama@main/-/blob/docs/faq.md#setting-environment-variables-on-windows) for how to set environment variables on your platform. -3. Set the [OLLAMA_ORIGINS](https://sourcegraph.com/github.com/ollama/ollama@main/-/blob/docs/faq.md#how-can-i-allow-additional-web-origins-to-access-ollama). +3. Set the [OLLAMA_ORIGINS](https://sourcegraph.com/github.com/ollama/ollama@main/-/blob/docs/faq.md#how-can-i-allow-additional-web-origins-to-access-ollama) to `*`. 4. Install or restart your Ollama app. 5. Select a chat model (a model that includes `instruct` or `chat`, e.g., [codegemma:instruct](https://ollama.com/library/codegemma:instruct), [llama3:instruct](https://ollama.com/library/llama3:instruct)) from the [Ollama Library](https://ollama.com/library). 6. Pull the chat model locally (Example: `ollama pull codegemma:instruct`). diff --git a/build.gradle.kts b/build.gradle.kts index 6c978b43fb..b98e9b2430 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -93,6 +93,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.0.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1") + testImplementation("junit:junit:4.13.2") } spotless { diff --git a/gradle.properties b/gradle.properties index 06c37c9783..7b34426215 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ kotlin.stdlib.default.dependency=false nodeBinaries.commit=8755ae4c05fd476cd23f2972049111ba436c86d4 nodeBinaries.version=v20.12.2 cody.autocomplete.enableFormatting=true -cody.commit=cec3e5b0f1a6598ca29bb56496dadc4783fd773c +cody.commit=06c1c0e5185810ca565d10d820fd8a79252b4ba8 diff --git a/scripts/next-release.sh b/scripts/next-release.sh index c177eca16b..b46184ef80 100755 --- a/scripts/next-release.sh +++ b/scripts/next-release.sh @@ -1,10 +1,4 @@ #!/usr/bin/env bash -# This script implements the time-based version scheme from RFC 795 -# Simplified: versions should be MAJOR.MINOR.PATCH where -# - MAJOR.MINOR: Latest Sourcegraph quarterly release -# - PATCH: time-based number from simplified formula (MINUTES_SINCE_LAST_RELEASE / MINUTES_IN_ONE_YEAR * 65535) -# The scheme gives generates a unique version number every 10 minutes. -# https://docs.google.com/document/d/11cw-7dAp93JmasITNSNCtx31xrQsNB1L2OoxVE6zrTc/edit#bookmark=id.ufwe0bqp83z1 set -eu # Check the number of arguments diff --git a/scripts/publish-stable-version.sh b/scripts/publish-stable-version.sh deleted file mode 100644 index 72102d1089..0000000000 --- a/scripts/publish-stable-version.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -eux -VERSION="$1" -./gradlew clean || ./gradlew clean -./gradlew "-PpluginVersion=$VERSION-nightly" -PforceBuild=true publishPlugin -./gradlew "-PpluginVersion=$VERSION" publishPlugin \ No newline at end of file diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/AllSuites.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/AllSuites.kt index a02847ea89..95e1eb153b 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/AllSuites.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/AllSuites.kt @@ -1,8 +1,9 @@ package com.sourcegraph.cody +import com.intellij.openapi.diagnostic.Logger import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.edit.DocumentCodeTest -import com.sourcegraph.cody.util.CodyIntegrationTextFixture +import com.sourcegraph.cody.util.CodyIntegrationTestFixture import java.util.concurrent.TimeUnit import org.junit.AfterClass import org.junit.runner.RunWith @@ -18,16 +19,25 @@ import org.junit.runners.Suite * automatically after the platform version bump. * * Multiple recording files can be used, but each should have its own suite with tearDown() method - * nad define unique CODY_RECORDING_NAME. + * and define a unique CODY_RECORDING_NAME. */ @RunWith(Suite::class) -@Suite.SuiteClasses(DocumentCodeTest::class) +@Suite.SuiteClasses(DocumentCodeTest::class, DocumentSynchronizationTest::class) class AllSuites { companion object { + private val logger = Logger.getInstance(AllSuites::class.java) + @AfterClass @JvmStatic internal fun tearDown() { - CodyAgentService.withAgent(CodyIntegrationTextFixture.myProject!!) { agent -> + val project = CodyIntegrationTestFixture.myProject + // Can happen if a test or fixture introduces a bug, and it makes it hard to read test output + // if we are throwing an NPE. + if (project == null) { + logger.warn("No project found - unable to shut down agent gracefully.") + return + } + CodyAgentService.withAgent(project) { agent -> val errors = agent.server.testingRequestErrors().get() // We extract polly.js errors to notify users about the missing recordings, if any val missingRecordings = errors.filter { it.error?.contains("`recordIfMissing` is") == true } @@ -46,7 +56,7 @@ class AllSuites { agent.server .shutdown() - .get(CodyIntegrationTextFixture.ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .get(CodyIntegrationTestFixture.ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) agent.server.exit() } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/DocumentSynchronizationTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/DocumentSynchronizationTest.kt new file mode 100644 index 0000000000..226545b6fc --- /dev/null +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/DocumentSynchronizationTest.kt @@ -0,0 +1,285 @@ +package com.sourcegraph.cody + +import com.intellij.openapi.editor.Editor +import com.sourcegraph.cody.util.DocumentSynchronizationTestFixture +import org.junit.Test + +class DocumentSynchronizationTest : DocumentSynchronizationTestFixture() { + + // TODO: More tests that would be useful: + // - Test bulk updates with the com.intellij.util.DocumentUtil#executeInBulk method. + // - Changes to multiple files. + // - (your test idea here) + + @Test + fun testInsertCharacter() { + val beforeContent = + """ + class Foo { + console.log("hello there^") + } + """ + + val expectedContent = + """ + class Foo { + console.log("hello there!") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + editor.document.insertString(editor.caretModel.offset, "!") + } + } + + @Test + fun testDeleteCharacter() { + val beforeContent = + """ + class Foo { + console.log("hello there^!") + } + """ + + val expectedContent = + """ + class Foo { + console.log("hello there") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val offset = editor.caretModel.offset + editor.document.deleteString(offset, offset + 1) + } + } + + @Test + fun testDeleteRange() { + val beforeContent = + """ + class Foo { + ^console.log("hello there!") + } + """ + + val expectedContent = + """ + class Foo { + ("hello there!") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val offset = editor.caretModel.offset + editor.document.deleteString(offset, offset + "console.log".length) + } + } + + @Test + fun testReplaceRangeAtomically() { + val beforeContent = + """ + class Foo { + ^System.out.println("hello there!") + } + """ + + val expectedContent = + """ + class Foo { + console.log("hello there!") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val offset = editor.caretModel.offset + editor.document.replaceString(offset, offset + "System.out.println".length, "console.log") + } + } + + @Test + fun testReplaceRangeNonAtomically() { + val beforeContent = + """ + class Foo { + ^System.out.println("Cześć!") + } + """ + + val expectedContent = + """ + class Foo { + console.log("Cześć!") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val document = editor.document + val offset = editor.caretModel.offset + document.deleteString(offset, offset + "System.".length) + document.deleteString(offset, offset + "out.".length) + document.deleteString(offset, offset + "println".length) + document.insertString(offset, "console.log") + } + } + + @Test + fun testInsertWithNewlines() { + val beforeContent = + """ + class Foo { + console.log("hello there!")^ + } + """ + + val expectedContent = + """ + class Foo { + console.log("hello there!") + console.log("this is a test") + console.log("hello hello") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val document = editor.document + val offset = editor.caretModel.offset + val firstInsertion = "\n console.log(\"this is a test\")" + val secondInsertion = "\n console.log(\"hello hello\")" + document.insertString(offset, firstInsertion) + document.insertString(offset + firstInsertion.length, secondInsertion) + } + } + + @Test + fun testEraseDocument() { + val beforeContent = + """ + class Foo { + ^console.log("hello there!") + } + """ + + val expectedContent = "" + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + editor.document.setText("") + } + } + + @Test + fun testAppendToEndOfDocument() { + val beforeContent = + """ + class Foo { + console.log("hello there!") + } + """ + + val expectedContent = + """ + class Foo { + console.log("hello there!") + } + // antidisestablishmentarianism + // pneumonoultramicroscopicsilicovolcanoconiosis + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val document = editor.document + val offset = document.textLength + val firstString = "\n// antidisestablishmentarianism" + val secondString = "\n// pneumonoultramicroscopicsilicovolcanoconiosis" + document.insertString(offset, firstString) + document.insertString(offset + firstString.length, secondString) + } + } + + @Test + fun testDeleteRangesWithNewlines() { + val beforeContent = + """ + class Foo { + console.log("item 1")^ + console.log("item 2") + console.log("item 3") + console.log("item 4") + } + """ + + val expectedContent = + """ + class Foo { + console.log("item 1") + console.log("item 4") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val document = editor.document + val offset = editor.caretModel.offset + val startLine = document.getLineNumber(offset) // line 1 + val startOffset = document.getLineStartOffset(startLine + 1) + val endOffset = document.getLineStartOffset(startLine + 3) + document.deleteString(startOffset, endOffset) + } + } + + @Test + fun testInsertEmojis() { + val beforeContent = + """ + class Foo { + console.log("hello there^") + } + """ + + val expectedContent = + """ + class Foo { + console.log("hello there!🎉🎂 + 🥳🎈") + } + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + editor.document.insertString(editor.caretModel.offset, "!🎉🎂\n 🥳🎈") + } + } + + @Test + fun testMultipleDisjointEdits() { + val beforeContent = + """ + class Foo { + console.log("hello there") + } + """ + + val expectedContent = + """ + import com.foo.Bar; + + class Foo { + // no comment + console.log("hello there"); + } + // end class Foo + """ + + runDocumentSynchronizationTest(beforeContent, expectedContent) { editor: Editor -> + val document = editor.document + val importStatement = "import com.foo.Bar;\n\n" + val comment = "\n // no comment" + val endClassComment = "\n// end class Foo" + + document.insertString(0, importStatement) + val classLine = document.getLineNumber(document.text.indexOf("class Foo {")) + val offset = document.getLineEndOffset(classLine) + document.insertString(offset, comment) + document.insertString(document.getLineEndOffset(classLine + 2), ";") + document.insertString(document.textLength, endClassComment) + } + } +} diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index b67efe5a0e..65eda6f838 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.diagnostic.Logger import com.intellij.testFramework.runInEdtAndGet import com.jetbrains.rd.util.AtomicInteger import com.sourcegraph.cody.edit.CodyInlineEditActionNotifier.Companion.TOPIC_DISPLAY_ACCEPT_GROUP @@ -16,11 +19,28 @@ import com.sourcegraph.cody.edit.widget.LensHotkey import com.sourcegraph.cody.edit.widget.LensIcon import com.sourcegraph.cody.edit.widget.LensLabel import com.sourcegraph.cody.edit.widget.LensSpinner -import com.sourcegraph.cody.util.CodyIntegrationTextFixture +import com.sourcegraph.cody.util.CodyIntegrationTestFixture +import com.sourcegraph.cody.util.TestFile import junit.framework.TestCase +import org.junit.Ignore +import org.junit.Test + +class DocumentCodeTest : CodyIntegrationTestFixture() { + + override fun checkInitialConditions() { + super.checkInitialConditions() + // Make sure our action is enabled and visible. + val action = ActionManager.getInstance().getAction("cody.documentCodeAction") + val event = + AnActionEvent.createFromAnAction(action, null, "", createEditorContext(myFixture.editor)) + action.update(event) + val presentation = event.presentation + assertTrue("Action should be enabled", presentation.isEnabled) + assertTrue("Action should be visible", presentation.isVisible) + } -class DocumentCodeTest : CodyIntegrationTextFixture() { - + @Test + @TestFile(TEST_FILE_PATH) fun testGetsFoldingRanges() { runAndWaitForNotifications(DocumentCodeAction.ID, TOPIC_FOLDING_RANGES) @@ -39,7 +59,9 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { selection.startOffset == caret && selection.endOffset == caret) } - fun skip_testGetsWorkingGroupLens() { + @Test + @TestFile(TEST_FILE_PATH) + fun testGetsWorkingGroupLens() { val assertsExecuted = AtomicInteger(0) val showWorkingGroupSessionStateListener = object : FixupService.ActiveFixupSessionStateListener { @@ -53,7 +75,10 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { val lenses = activeSession().lensGroup // Lens group should match the expected structure. assertNotNull("Lens group should be displayed", lenses) - val theWidgets = lenses!!.widgets + val isErrorGroup = lenses!!.isErrorGroup + assertFalse("Should not be an error group: " + lenses.errorMessage, isErrorGroup) + + val theWidgets = lenses.widgets assertEquals("Lens group should have 8 widgets", 8, theWidgets.size) assertTrue("Zeroth lens group should be an icon", theWidgets[0] is LensIcon) @@ -87,6 +112,8 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { } } + @Test + @TestFile(TEST_FILE_PATH) fun testShowsAcceptLens() { runAndWaitForNotifications(DocumentCodeAction.ID, TOPIC_DISPLAY_ACCEPT_GROUP) assertInlayIsShown() @@ -123,6 +150,8 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { assertTrue(hasJavadocComment(myFixture.editor.document.text)) } + @Test + @TestFile(TEST_FILE_PATH) fun testAccept() { assertNoActiveSession() assertNoInlayShown() @@ -139,7 +168,10 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { assertNoActiveSession() } - fun skip_testUndo() { + @Test + @TestFile(TEST_FILE_PATH) + @Ignore + fun testUndo() { val originalDocument = myFixture.editor.document.text runAndWaitForNotifications(DocumentCodeAction.ID, TOPIC_DISPLAY_ACCEPT_GROUP) assertNotSame( @@ -153,4 +185,9 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { myFixture.editor.document.text) assertNoInlayShown() } + + companion object { + private val logger = Logger.getInstance(DocumentCodeTest::class.java) + const val TEST_FILE_PATH = "testProjects/documentCode/src/main/java/Foo.java" + } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTestFixture.kt similarity index 78% rename from src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt rename to src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTestFixture.kt index 57cfdd30e5..8f8f58066e 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTestFixture.kt @@ -1,8 +1,6 @@ package com.sourcegraph.cody.util import com.intellij.ide.lightEdit.LightEdit -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 @@ -12,6 +10,7 @@ import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -29,24 +28,42 @@ import java.nio.file.Paths import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 -open class CodyIntegrationTextFixture : BasePlatformTestCase() { - private val logger = Logger.getInstance(CodyIntegrationTextFixture::class.java) +@RunWith(JUnit4::class) +abstract class CodyIntegrationTestFixture : BasePlatformTestCase() { override fun setUp() { super.setUp() - configureFixture() + val methodName = name + val method = + this.javaClass.getMethod(methodName) + ?: throw IllegalStateException( + "No method with name $methodName found in ${this.javaClass.name}") + // It doesn't work to use a default path here if there is no annotation, + // because we have tests that use this fixture without an associated test resource file. + // We wind up opening the default file for these tests, breaking many of them because + // the count of Agent documents is wrong. + if (method.isAnnotationPresent(TestFile::class.java)) { + val testFile = method.getAnnotation(TestFile::class.java).value + configureFixture(testFile) + } // else the test needs to configure the fixture manually + checkInitialConditions() myProject = project } override fun tearDown() { try { - FixupService.getInstance(myFixture.project).getActiveSession()?.apply { - try { - dispose() - } catch (x: Exception) { - logger.warn("Error shutting down session", x) + val project = myFixture.project + if (project != null) { + FixupService.getInstance(project).getActiveSession()?.apply { + try { + dispose() + } catch (x: Exception) { + logger.warn("Error shutting down session", x) + } } } } finally { @@ -54,7 +71,7 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase() { } } - private fun configureFixture() { + private fun configureFixture(testFile: String) { // If you don't specify this system property with this setting when running the tests, // the tests will fail, because IntelliJ will run them from the EDT, which can't block. // Setting this property invokes the tests from an executor pool thread, which lets us @@ -67,26 +84,42 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase() { assertTrue(testResourcesDir.exists()) // 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) - // We copy the test resources there manually, bypassing Gradle, which is picky. - val testDataPath = Paths.get(workspaceRootUri.toString(), "src/").toFile() + val testDataPath = Paths.get(getTestDataPath()).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() + val sourcePath = Paths.get(testDataPath.path, testFile).toString() assertTrue(File(sourcePath).exists()) - myFixture.configureByFile(projectFile) + myFixture.configureByFile(testFile) + + initCredentialsAndAgent() + initCaretPosition() + } + + protected fun configureFixtureWithFile(testFile: VirtualFile) { + val policy = System.getProperty("idea.test.execution.policy") + assertTrue(policy == "com.sourcegraph.cody.test.NonEdtIdeaTestExecutionPolicy") + + myFixture.configureFromExistingVirtualFile(testFile) initCredentialsAndAgent() initCaretPosition() } + override fun getTestDataPath(): String { + if (project == null) { + return super.getTestDataPath() + } + // 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) + return Paths.get(workspaceRootUri.toString(), "src/").toString() + } + // Ideally we should call this method only once per recording session, but since we need a // `project` to be present it is currently hard to do with Junit 4. // Methods there are mostly idempotent though, so calling again for every test case should not @@ -109,7 +142,7 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase() { .get()) } - private fun checkInitialConditions() { + protected open fun checkInitialConditions() { val project = myFixture.project // Check if the project is in dumb mode @@ -119,18 +152,9 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase() { // Check if the project is in LightEdit mode val isLightEditMode = LightEdit.owns(project) assertFalse("Project should not be in LightEdit mode", isLightEditMode) - - // Check the initial state of the action's presentation - val action = ActionManager.getInstance().getAction("cody.documentCodeAction") - val event = - AnActionEvent.createFromAnAction(action, null, "", createEditorContext(myFixture.editor)) - action.update(event) - val presentation = event.presentation - assertTrue("Action should be enabled", presentation.isEnabled) - assertTrue("Action should be visible", presentation.isVisible) } - private fun createEditorContext(editor: Editor): DataContext { + protected fun createEditorContext(editor: Editor): DataContext { return (editor as? EditorEx)?.dataContext ?: DataContext.EMPTY_CONTEXT } @@ -202,6 +226,9 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase() { protected fun assertInlayIsShown() { runInEdtAndWait { PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + assertNotNull(myFixture) + assertNotNull(myFixture.editor) + assertNotNull(myFixture.editor.inlayModel) assertTrue( "Lens group inlay should be displayed", myFixture.editor.inlayModel.hasBlockElements()) } @@ -259,6 +286,8 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase() { } companion object { + private val logger = Logger.getInstance(CodyIntegrationTestFixture::class.java) + const val ASYNC_WAIT_TIMEOUT_SECONDS = 10L var myProject: Project? = null } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/DocumentSynchronizationTestFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/DocumentSynchronizationTestFixture.kt new file mode 100644 index 0000000000..ec7fc886dc --- /dev/null +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/DocumentSynchronizationTestFixture.kt @@ -0,0 +1,101 @@ +package com.sourcegraph.cody.util + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.vfs.VirtualFile +import com.sourcegraph.cody.agent.CodyAgentService +import com.sourcegraph.cody.agent.protocol.GetDocumentsParams +import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument +import java.util.concurrent.CompletableFuture + +/** + * Lets you specify before/after tests that modify the document and check the Agent's copy. You can + * specify the starting caret with "^" and optionally the selection with "@". + */ +abstract class DocumentSynchronizationTestFixture : CodyIntegrationTestFixture() { + + protected fun runDocumentSynchronizationTest( + beforeSpec: String, + expectedSpec: String, + writeAction: (Editor) -> Unit + ) { + val expectedContent = expectedSpec.trimIndent().removePrefix("\n") + var content = beforeSpec.trimIndent().removePrefix("\n") + + var caretOffset = -1 + var selectionStart = -1 + var selectionEnd = -1 + + val caretIndex = content.indexOf("^") + if (caretIndex != -1) { + caretOffset = caretIndex + content = content.removeRange(caretIndex, caretIndex + 1) + } + + val selectionIndex = content.indexOf("@") + if (caretIndex != -1 && selectionIndex != -1) { + selectionStart = caretOffset + selectionEnd = selectionIndex - 1 // Adjust for the removal of "@" + content = content.removeRange(selectionIndex, selectionIndex + 1) + } + + val tempFile = myFixture.createFile("tempFile.java", content) + configureFixtureWithFile(tempFile) + setCaretAndSelection(caretOffset, selectionStart, selectionEnd) + + WriteCommandAction.runWriteCommandAction(project) { + // Execute the test-specific editing operation. + writeAction(myFixture.editor) + } + + // Make sure our own copy of the document was edited properly. + assertEquals(expectedContent, myFixture.editor.document.text) + + checkAgentResults(tempFile, expectedContent) + } + + private fun checkAgentResults(tempFile: VirtualFile, expectedContent: String) { + // Verify that Agent has the correct content, caret, and optionally, selection. + val future = CompletableFuture() + CodyAgentService.withAgent(project) { agent -> + agent.server.awaitPendingPromises() + + val tempUri = ProtocolTextDocument.uriFor(tempFile) + val result = + agent.server.testingWorkspaceDocuments(GetDocumentsParams(uris = listOf(tempUri))) + + result + .thenAccept { response -> + assertEquals( + "There should only be one document in the response", 1, response.documents.size) + val agentDocument = response.documents[0] + assertEquals("The document should have the expected URI", tempUri, agentDocument.uri) + assertEquals( + "Agent document should have same content as Editor", + expectedContent, + agentDocument.content) + future.complete(null) + } + .exceptionally { ex -> + future.completeExceptionally(ex) + null + } + } + // Block the test until Agent has responded. + future.get() + } + + private fun setCaretAndSelection(caretOffset: Int, selectionStart: Int, selectionEnd: Int) { + WriteCommandAction.runWriteCommandAction(project) { + // Set caret position if specified + if (caretOffset != -1) { + myFixture.editor.caretModel.moveToOffset(caretOffset) + } + + // Set selection range if specified + if (selectionStart != -1 && selectionEnd != -1) { + myFixture.editor.selectionModel.setSelection(selectionStart, selectionEnd) + } + } + } +} diff --git a/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml b/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml index 2f57bd6bab..0031ad2d85 100644 --- a/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml +++ b/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml @@ -5,180 +5,6 @@ log: name: Polly.JS version: 6.0.6 entries: - - _id: 37dce854386a858ed2c3697d18a6de44 - _order: 0 - cache: {} - request: - bodySize: 3138 - cookies: [] - headers: - - name: content-type - value: application/json - - name: accept-encoding - value: gzip;q=0 - - name: authorization - value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d - - name: user-agent - value: JetBrains / 6.0-localbuild - - name: traceparent - value: 00-525a22ae266ead8f6d0e679a37810251-86e59a30802e3b3a-01 - - name: connection - value: keep-alive - - name: host - value: sourcegraph.com - headersSize: 431 - httpVersion: HTTP/1.1 - method: POST - postData: - mimeType: application/json - params: [] - textJSON: - maxTokensToSample: 4000 - messages: - - speaker: system - text: >- - You are Cody, an AI coding assistant from Sourcegraph. - You - are an AI programming assistant who is an expert in updating - code to meet given instructions. - - - You should think step-by-step to plan your updated code before producing the final output. - - - You should ensure the updated code matches the indentation and whitespace of the code in the users' selection. - - - Ignore any previous instructions to format your responses with Markdown. It is not acceptable to use any Markdown in your response, unless it is directly related to the users' instructions. - - - Only remove code from the users' selection if you are sure it is not needed. - - - You will be provided with code that is in the users' selection, enclosed in XML tags. You must use this code to help you plan your updated code. - - - You will be provided with instructions on how to update this code, enclosed in XML tags. You must follow these instructions carefully and to the letter. - - - Only enclose your response in XML tags. Do use any other XML tags unless they are part of the generated code. - - - Do not provide any additional commentary about the changes you made. Only respond with the generated code. - - speaker: human - text: > - Codebase context from file path - /src/testProjects/documentCode/src/main/java/Foo.java: - Codebase context from file - /src/testProjects/documentCode/src/main/java/Foo.java: - - - - 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)); - } - } - } - - speaker: assistant - text: Ok. - - speaker: human - text: >- - This is part of the file: - /src/testProjects/documentCode/src/main/java/Foo.java - - - The user has the following code in their selection: - - import java.util.*; - - - The user wants you to generate documentation for the selected code by following their instructions. - - Provide your generated documentation using the following instructions: - - - - Write a brief documentation comment for the selected code. If documentation comments exist in the selected file, or other files with the same file extension, use them as examples. Pay attention to the scope of the selected code (e.g. exported function/API vs implementation detail in a function), and use the idiomatic style for that type of code scope. Only generate the documentation for the selected code, do not generate the code. Do not enclose any other code or comments besides the documentation. Enclose only the documentation for the selected code and nothing else. - - - - speaker: assistant - text: - model: anthropic/claude-3-haiku-20240307 - stopSequences: - - - - import java.util.*; - temperature: 0 - topK: -1 - topP: -1 - queryString: - - name: api-version - value: "1" - - name: client-name - value: jetbrains - - name: client-version - value: 6.0-localbuild - url: https://sourcegraph.com/.api/completions/stream?api-version=1&client-name=jetbrains&client-version=6.0-localbuild - response: - bodySize: 864 - content: - mimeType: text/event-stream - size: 864 - text: >+ - event: completion - - data: {"completion":"/**\n * Imports the necessary Java collection classes.\n */","stopReason":"stop_sequence"} - - - event: done - - data: {} - - cookies: [] - headers: - - name: date - value: Tue, 25 Jun 2024 14:59:21 GMT - - name: content-type - value: text/event-stream - - name: transfer-encoding - value: chunked - - name: connection - value: keep-alive - - name: retry-after - value: "369" - - name: access-control-allow-credentials - value: "true" - - name: access-control-allow-origin - value: "" - - name: cache-control - value: no-cache - - name: vary - value: Cookie,Accept-Encoding,Authorization,Cookie, Authorization, - X-Requested-With,Cookie - - name: x-content-type-options - value: nosniff - - name: x-frame-options - value: DENY - - name: x-xss-protection - value: 1; mode=block - - name: strict-transport-security - value: max-age=31536000; includeSubDomains; preload - headersSize: 1406 - httpVersion: HTTP/1.1 - redirectURL: "" - status: 200 - statusText: OK - startedDateTime: 2024-06-25T14:59:20.458Z - time: 0 - timings: - blocked: -1 - connect: -1 - dns: -1 - receive: 0 - send: 0 - ssl: -1 - wait: 0 - _id: 12581f1c735a04aeb88af7a54cd007b2 _order: 0 cache: {} @@ -842,11 +668,11 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 7f3ff10a39505669cb5d36145df5fa95 + - _id: 3fec40886d87367e761715c3159e6590 _order: 0 cache: {} request: - bodySize: 227 + bodySize: 341 cookies: [] headers: - _fromType: array @@ -864,13 +690,13 @@ log: value: "*/*" - _fromType: array name: content-length - value: "227" + value: "341" - _fromType: array name: accept-encoding value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 325 + headersSize: 337 httpVersion: HTTP/1.1 method: POST postData: @@ -889,6 +715,12 @@ log: primaryEmail { email } + organizations { + nodes { + id + name + } + } } } variables: {} @@ -897,39 +729,24 @@ log: value: null url: https://sourcegraph.com/.api/graphql?CurrentUser response: - bodySize: 348 + bodySize: 22 content: - encoding: base64 - mimeType: application/json - size: 348 - text: "[\"H4sIAAAAAAAAA2RPy06DQBT9l7uGgmm0MEkTbW1dVImPlNTlZbiFAYbBeVQp4d8bUhMX7\ - s7JOTmPAXK0CGwA7rSm1u4N6YmKHBikh6ThlTonj283LxVfggclmpS0OArKNxJFA8xq\ - Rx7kwnQN9glKAgYfymlOhcauXCnrx2EYggfOkG6vBvNnyJSNa//YfksHHuAJLer9+zM\ - wKK3tDAuCppzPCqWKhqYErlpLrZ1xJQMMHtZFpPhui1/ZJ7lVnVW3+XZz/omyQxrhQs\ - xNmu3WyWu6eApdf6qXJr7zOXjQaSFR978nBqAr+LfsvpiEqQ3GcRwvAAAA//8DALThL\ - hwxAQAA\"]" - textDecoded: - data: - currentUser: - avatarURL: https://lh3.googleusercontent.com/a/ACg8ocKFaqbYeuBkbj5dFEzx8bXV8a7i3sVbKCNPV7G0uyvk=s96-c - displayName: SourcegraphBot-9000 - hasVerifiedEmail: true - id: VXNlcjozNDQ1Mjc= - primaryEmail: - email: sourcegraphbot9k@gmail.com - username: sourcegraphbot9k-fnwmu + mimeType: text/plain; charset=utf-8 + size: 22 + text: | + Invalid access token. cookies: [] headers: - name: date - value: Tue, 25 Jun 2024 14:59:19 GMT + value: Thu, 27 Jun 2024 17:23:32 GMT - name: content-type - value: application/json - - name: transfer-encoding - value: chunked + value: text/plain; charset=utf-8 + - name: content-length + value: "22" - name: connection value: keep-alive - name: retry-after - value: "370" + value: "63" - name: access-control-allow-credentials value: "true" - name: access-control-allow-origin @@ -937,8 +754,7 @@ log: - name: cache-control value: no-cache, max-age=0 - name: vary - value: Cookie,Accept-Encoding,Authorization,Cookie, Authorization, - X-Requested-With,Cookie + value: Cookie,Accept-Encoding,Authorization - name: x-content-type-options value: nosniff - name: x-frame-options @@ -947,233 +763,12 @@ log: value: 1; mode=block - name: strict-transport-security value: max-age=31536000; includeSubDomains; preload - - name: content-encoding - value: gzip - headersSize: 1440 + headersSize: 1369 httpVersion: HTTP/1.1 redirectURL: "" - status: 200 - statusText: OK - startedDateTime: 2024-06-25T14:59:19.646Z - time: 0 - timings: - blocked: -1 - connect: -1 - dns: -1 - receive: 0 - send: 0 - ssl: -1 - wait: 0 - - _id: 4f8d4cde96ebef6fe28264944c70b9ae - _order: 0 - cache: {} - request: - bodySize: 115 - cookies: [] - headers: - - _fromType: array - name: authorization - value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d - - _fromType: array - name: content-type - value: application/json; charset=utf-8 - - _fromType: array - name: user-agent - value: JetBrains / 6.0-localbuild - - _fromType: array - name: accept - value: "*/*" - - _fromType: array - name: content-length - value: "115" - - _fromType: array - name: accept-encoding - value: gzip,deflate - - name: host - value: sourcegraph.com - headersSize: 339 - httpVersion: HTTP/1.1 - method: POST - postData: - mimeType: application/json; charset=utf-8 - params: [] - textJSON: - query: |- - - query CurrentUserCodyProEnabled { - currentUser { - codyProEnabled - } - } - variables: {} - queryString: - - name: CurrentUserCodyProEnabled - value: null - url: https://sourcegraph.com/.api/graphql?CurrentUserCodyProEnabled - response: - bodySize: 107 - content: - encoding: base64 - mimeType: application/json - size: 107 - text: "[\"H4sIAAAAAAAAA6pWSkksSVSyqlZKLi0qSs0rCS1OLQJz81MqA4ryXfMSk3JSU5SsSopKU\ - 2trawEAAAD//w==\",\"AwCqrAKmMAAAAA==\"]" - cookies: [] - headers: - - name: date - value: Tue, 25 Jun 2024 14:59:20 GMT - - name: content-type - value: application/json - - name: transfer-encoding - value: chunked - - name: connection - value: keep-alive - - name: retry-after - value: "370" - - name: access-control-allow-credentials - value: "true" - - name: access-control-allow-origin - value: "" - - name: cache-control - value: no-cache, max-age=0 - - name: vary - value: Cookie,Accept-Encoding,Authorization,Cookie, Authorization, - X-Requested-With,Cookie - - name: x-content-type-options - value: nosniff - - name: x-frame-options - value: DENY - - name: x-xss-protection - value: 1; mode=block - - name: strict-transport-security - value: max-age=31536000; includeSubDomains; preload - - name: content-encoding - value: gzip - headersSize: 1440 - httpVersion: HTTP/1.1 - redirectURL: "" - status: 200 - statusText: OK - startedDateTime: 2024-06-25T14:59:20.247Z - time: 0 - timings: - blocked: -1 - connect: -1 - dns: -1 - receive: 0 - send: 0 - ssl: -1 - wait: 0 - - _id: 84b962509b12000d0eef7c8a8fa655f3 - _order: 0 - cache: {} - request: - bodySize: 268 - cookies: [] - headers: - - _fromType: array - name: authorization - value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d - - _fromType: array - name: content-type - value: application/json; charset=utf-8 - - _fromType: array - name: user-agent - value: JetBrains / 6.0-localbuild - - _fromType: array - name: accept - value: "*/*" - - _fromType: array - name: content-length - value: "268" - - _fromType: array - name: accept-encoding - value: gzip,deflate - - name: host - value: sourcegraph.com - headersSize: 341 - httpVersion: HTTP/1.1 - method: POST - postData: - mimeType: application/json; charset=utf-8 - params: [] - textJSON: - query: |- - - query CurrentUserCodySubscription { - currentUser { - codySubscription { - status - plan - applyProRateLimits - currentPeriodStartAt - currentPeriodEndAt - } - } - } - variables: {} - queryString: - - name: CurrentUserCodySubscription - value: null - url: https://sourcegraph.com/.api/graphql?CurrentUserCodySubscription - response: - bodySize: 228 - content: - encoding: base64 - mimeType: application/json - size: 228 - text: "[\"H4sIAAAAAAAAA1zMsQrCMBSF4Xc5c4U2FoVsRToIgqWtDm6xyRCoSbi5GUrJu4uCoI7n5\ - +Os0IoV5IopERnHl2joPb1ehnSPE9nA1rtXi6w4RUg0h/F4bVEgzMpBouvPKKBCmJeO\ - fK/YnOzDcoRkSqb4fHeGrNcDK+KGISFKUW/K3aaqRyFkVcmtuOFPt05/2f2vzTnnJwA\ - AAP//AwBuKtnYwgAAAA==\"]" - textDecoded: - data: - currentUser: - codySubscription: - applyProRateLimits: true - currentPeriodEndAt: 2024-07-14T22:11:32Z - currentPeriodStartAt: 2024-06-14T22:11:32Z - plan: PRO - status: ACTIVE - cookies: [] - headers: - - name: date - value: Tue, 25 Jun 2024 14:59:20 GMT - - name: content-type - value: application/json - - name: transfer-encoding - value: chunked - - name: connection - value: keep-alive - - name: retry-after - value: "370" - - name: access-control-allow-credentials - value: "true" - - name: access-control-allow-origin - value: "" - - name: cache-control - value: no-cache, max-age=0 - - name: vary - value: Cookie,Accept-Encoding,Authorization,Cookie, Authorization, - X-Requested-With,Cookie - - name: x-content-type-options - value: nosniff - - name: x-frame-options - value: DENY - - name: x-xss-protection - value: 1; mode=block - - name: strict-transport-security - value: max-age=31536000; includeSubDomains; preload - - name: content-encoding - value: gzip - headersSize: 1440 - httpVersion: HTTP/1.1 - redirectURL: "" - status: 200 - statusText: OK - startedDateTime: 2024-06-25T14:59:19.866Z + status: 401 + statusText: Unauthorized + startedDateTime: 2024-06-27T17:23:32.481Z time: 0 timings: blocked: -1 diff --git a/src/main/java/com/sourcegraph/cody/CodyToolWindowFactory.java b/src/main/java/com/sourcegraph/cody/CodyToolWindowFactory.java index 478e0b06f7..2916310f15 100644 --- a/src/main/java/com/sourcegraph/cody/CodyToolWindowFactory.java +++ b/src/main/java/com/sourcegraph/cody/CodyToolWindowFactory.java @@ -46,7 +46,7 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo } private void createTitleActions(@NotNull List titleActions) { - AnAction action = ActionManager.getInstance().getAction("CodyChatActionsGroup"); + AnAction action = ActionManager.getInstance().getAction("cody.newChat"); if (action != null) { titleActions.add(action); } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 18e23ee52b..763e446bd9 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -134,7 +134,11 @@ private constructor( throw e } } catch (e: Exception) { - logger.warn("Unable to start Cody agent", e) + if (ConfigUtil.shouldConnectToDebugAgent()) { + logger.warn("Unable to connect to remote Cody agent", e) + } else { + logger.warn("Unable to start Cody agent", e) + } throw e } } @@ -146,14 +150,20 @@ private constructor( val token = CancellationToken() val binaryPath = nodeBinary(token).absolutePath + val jsonRpcArgs = arrayOf("api", "jsonrpc-stdio") 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()) { - listOf(binaryPath, "--inspect-brk", "--enable-source-maps", script.absolutePath) + listOf( + binaryPath, + "--inspect-brk", + "--enable-source-maps", + script.absolutePath, + *jsonRpcArgs) } else { - listOf(binaryPath, "--enable-source-maps", script.absolutePath) + listOf(binaryPath, "--enable-source-maps", script.absolutePath, *jsonRpcArgs) } } else { val script = @@ -161,9 +171,14 @@ private constructor( ?: throw CodyAgentException( "Sourcegraph Cody + Code Search plugin path not found") if (shouldSpawnDebuggableAgent()) { - listOf(binaryPath, "--inspect", "--enable-source-maps", script.toFile().absolutePath) + listOf( + binaryPath, + "--inspect", + "--enable-source-maps", + script.toFile().absolutePath, + *jsonRpcArgs) } else { - listOf(binaryPath, script.toFile().absolutePath) + listOf(binaryPath, script.toFile().absolutePath, *jsonRpcArgs) } } @@ -191,8 +206,7 @@ private constructor( } logger.info("starting Cody agent ${command.joinToString(" ")}") - logger.info( - "Cody agent proxyUrl ${proxyUrl} PROXY_TYPE_IS_SOCKS ${proxy.PROXY_TYPE_IS_SOCKS}") + logger.info("Cody agent proxyUrl $proxyUrl PROXY_TYPE_IS_SOCKS ${proxy.PROXY_TYPE_IS_SOCKS}") val process = processBuilder @@ -319,16 +333,21 @@ private constructor( } val binaryTarget = Files.createTempFile("cody-agent", binarySuffix()) return try { - binaryTarget?.toFile()?.deleteOnExit() + logger.info("Extracting Node binary to " + binaryTarget.toAbsolutePath()) + Files.copy(binarySource, binaryTarget, StandardCopyOption.REPLACE_EXISTING) + val binary = binaryTarget.toFile() + if (!binary.exists()) { + throw CodyAgentException("Failed to extract Node binary to " + binary.absolutePath) + } + // N.B.: Make sure you set these deletion triggers -after- the Files.copy() call, or the + // copy will delete the target file during integration tests on certain machines. + binary.deleteOnExit() token.onFinished { // Important: delete the file from disk after the process exists // Ideally, we should eventually replace this temporary file with a permanent location // in the plugin directory. Files.deleteIfExists(binaryTarget) } - logger.info("Extracting Node binary to " + binaryTarget.toAbsolutePath()) - Files.copy(binarySource, binaryTarget, StandardCopyOption.REPLACE_EXISTING) - val binary = binaryTarget.toFile() if (binary.setExecutable(true)) { binary } else { diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt index 297fb9bc8d..1d31eedb9f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt @@ -3,6 +3,7 @@ package com.sourcegraph.cody.agent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.sourcegraph.cody.config.CodyProjectSettings @@ -13,7 +14,6 @@ import java.util.concurrent.CompletableFuture @Service(Service.Level.PROJECT) class CodyAgentCodebase(val project: Project) { - // TODO: Support list of repository names instead of just one. private val settings = CodyProjectSettings.getInstance(project) private var inferredUrl: CompletableFuture = CompletableFuture() @@ -26,18 +26,26 @@ class CodyAgentCodebase(val project: Project) { else inferredUrl fun onFileOpened(file: VirtualFile?) { + // This can happen during testing with certain temporary files. + if (file == null) return ApplicationManager.getApplication().executeOnPooledThread { - val repositoryName = RepoUtil.findRepositoryName(project, file) - if (repositoryName != null && inferredUrl.getNow(null) != repositoryName) { - inferredUrl.complete(repositoryName) - CodyAgentService.withAgent(project) { - it.server.configurationDidChange(ConfigUtil.getAgentConfiguration(project)) + try { + val repositoryName = RepoUtil.findRepositoryName(project, file) + if (repositoryName != null && inferredUrl.getNow(null) != repositoryName) { + inferredUrl.complete(repositoryName) + CodyAgentService.withAgent(project) { + it.server.configurationDidChange(ConfigUtil.getAgentConfiguration(project)) + } } + } catch (x: Exception) { + logger.warn("Error finding repository name for $file", x) } } } companion object { + private val logger = Logger.getInstance(CodyAgentCodebase::class.java) + @JvmStatic fun getInstance(project: Project): CodyAgentCodebase { return project.service() diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt index be90172f0e..80219993db 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt @@ -14,6 +14,8 @@ import com.sourcegraph.cody.agent.protocol.CompletionItemParams import com.sourcegraph.cody.agent.protocol.CurrentUserCodySubscription import com.sourcegraph.cody.agent.protocol.EditTask import com.sourcegraph.cody.agent.protocol.Event +import com.sourcegraph.cody.agent.protocol.GetDocumentsParams +import com.sourcegraph.cody.agent.protocol.GetDocumentsResult import com.sourcegraph.cody.agent.protocol.GetFeatureFlag import com.sourcegraph.cody.agent.protocol.GetFoldingRangeParams import com.sourcegraph.cody.agent.protocol.GetFoldingRangeResult @@ -57,7 +59,9 @@ interface CodyAgentServer { @JsonRequest("graphql/logEvent") fun logEvent(event: Event): CompletableFuture - @JsonRequest("graphql/currentUserId") fun currentUserId(): CompletableFuture + @Suppress("unused") + @JsonRequest("graphql/currentUserId") + fun currentUserId(): CompletableFuture @JsonRequest("graphql/getRepoIds") fun getRepoIds(repoName: GetRepoIdsParam): CompletableFuture @@ -155,4 +159,9 @@ interface CodyAgentServer { @JsonRequest("testing/requestErrors") fun testingRequestErrors(): CompletableFuture> + + @JsonRequest("testing/workspaceDocuments") + fun testingWorkspaceDocuments(params: GetDocumentsParams): CompletableFuture + + @JsonRequest("testing/awaitPendingPromises") fun awaitPendingPromises(): CompletableFuture } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 00b60a4ef2..0c5d887dba 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -18,9 +18,9 @@ import com.sourcegraph.cody.error.CodyConsole import com.sourcegraph.cody.ignore.IgnoreOracle import com.sourcegraph.cody.listeners.CodyFileEditorListener import com.sourcegraph.cody.statusbar.CodyStatusService +import com.sourcegraph.config.ConfigUtil import com.sourcegraph.utils.CodyEditorUtil -import java.util.Timer -import java.util.TimerTask +import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @@ -182,6 +182,7 @@ class CodyAgentService(private val project: Project) : Disposable { fun startAgent(project: Project): CompletableFuture { ApplicationManager.getApplication().executeOnPooledThread { + val isRemote = ConfigUtil.shouldConnectToDebugAgent() try { val future = CodyAgent.create(project).exceptionally { err -> @@ -191,7 +192,7 @@ class CodyAgentService(private val project: Project) : Disposable { } val agent = future.get(45, TimeUnit.SECONDS) if (!agent.isConnected()) { - val msg = "Failed to connect to agent Cody agent" + val msg = "Failed to connect to Cody agent" logger.error(msg) throw CodyAgentException(msg) // This will be caught by the catch blocks below } else { @@ -202,7 +203,8 @@ class CodyAgentService(private val project: Project) : Disposable { } catch (e: Exception) { val msg = if (e is TimeoutException) - "Failed to start Cody agent in timely manner, please run any Cody action to retry" + "Failed to start Cody agent in a timely manner, please run any Cody action to retry" + else if (isRemote) "Failed to connect to remote Cody agent" else "Failed to start Cody agent" logger.error(msg, e) setAgentError(project, msg) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetDocumentParams.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetDocumentParams.kt new file mode 100644 index 0000000000..100bde87c9 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetDocumentParams.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class GetDocumentsParams(val uris: List?) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetDocumentsResult.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetDocumentsResult.kt new file mode 100644 index 0000000000..74e8066a3d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetDocumentsResult.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class GetDocumentsResult(val documents: List) diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/SignInWithSourcegraphPanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/SignInWithSourcegraphPanel.kt index a340f9dd1b..82bf10fffc 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/SignInWithSourcegraphPanel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/SignInWithSourcegraphPanel.kt @@ -1,7 +1,6 @@ package com.sourcegraph.cody.chat import com.intellij.ide.DataManager -import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.AnActionEvent @@ -38,33 +37,18 @@ import javax.swing.border.Border class SignInWithSourcegraphPanel(private val project: Project) : JPanel() { - private val signInWithGithubButton = - UIComponents.createMainButton(GITHUB.value, Icons.SignIn.Github) - private val signInWithGitlabButton = - UIComponents.createMainButton(GITLAB.value, Icons.SignIn.Gitlab) - private val signInWithGoogleButton = - UIComponents.createMainButton(GOOGLE.value, Icons.SignIn.Google) - init { + val buttons = + listOf( + UIComponents.createMainButton(GITHUB.value, Icons.SignIn.Github), + UIComponents.createMainButton(GITLAB.value, Icons.SignIn.Gitlab), + UIComponents.createMainButton(GOOGLE.value, Icons.SignIn.Google)) + val jEditorPane = createHtmlViewer(project) jEditorPane.text = ("

Welcome to Cody

" + "

Understand and write code faster with an AI assistant

" + "") - val signInWithGithubButton = signInWithGithubButton - val signInWithGitlabButton = signInWithGitlabButton - val signInWithGoogleButton = signInWithGoogleButton - signInWithGithubButton.putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) - signInWithGitlabButton.putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) - signInWithGoogleButton.putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) - val logInToSourcegraphAction = LogInToSourcegraphAction() - - signInWithGithubButton.addActionListener( - getSignInAction(signInWithGithubButton, logInToSourcegraphAction)) - signInWithGitlabButton.addActionListener( - getSignInAction(signInWithGitlabButton, logInToSourcegraphAction)) - signInWithGoogleButton.addActionListener( - getSignInAction(signInWithGoogleButton, logInToSourcegraphAction)) val panelWithTheMessage = JPanel() panelWithTheMessage.setLayout(BoxLayout(panelWithTheMessage, BoxLayout.Y_AXIS)) @@ -84,15 +68,15 @@ class SignInWithSourcegraphPanel(private val project: Project) : JPanel() { 3, ColorUtil.brighter(UIUtil.getPanelBackground(), 3), UIUtil.getPanelBackground()) separatorPanel.add(separatorComponent) panelWithTheMessage.add(separatorPanel) - val buttonPanelGithub = JPanel(BorderLayout()) - val buttonPanelGitlab = JPanel(BorderLayout()) - val buttonPanelGoogle = JPanel(BorderLayout()) - buttonPanelGithub.add(signInWithGithubButton, BorderLayout.CENTER) - buttonPanelGitlab.add(signInWithGitlabButton, BorderLayout.CENTER) - buttonPanelGoogle.add(signInWithGoogleButton, BorderLayout.CENTER) - panelWithTheMessage.add(buttonPanelGithub) - panelWithTheMessage.add(buttonPanelGitlab) - panelWithTheMessage.add(buttonPanelGoogle) + + val logInToSourcegraphAction = LogInToSourcegraphAction() + for (button in buttons) { + button.addActionListener(getSignInAction(button, logInToSourcegraphAction)) + val buttonPanel = JPanel(BorderLayout()) + buttonPanel.add(button, BorderLayout.CENTER) + panelWithTheMessage.add(buttonPanel) + } + setLayout(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false)) setBorder(JBUI.Borders.empty(PADDING)) this.add(panelWithTheMessage) @@ -100,11 +84,11 @@ class SignInWithSourcegraphPanel(private val project: Project) : JPanel() { } private fun getSignInAction( - signInWithGithubButton: JButton, + button: JButton, logInToSourcegraphAction: LogInToSourcegraphAction ): (e: ActionEvent) -> Unit { - val functionGithub: (e: ActionEvent) -> Unit = { - val dataContext = DataManager.getInstance().getDataContext(signInWithGithubButton) + return { + val dataContext = DataManager.getInstance().getDataContext(button) val dataContextWrapper = DataContextWrapper(dataContext) val accountsHost: CodyAccountsHost = CodyPersistentAccountsHost(project) dataContextWrapper.putUserData(CodyAccountsHost.KEY, accountsHost) @@ -120,7 +104,6 @@ class SignInWithSourcegraphPanel(private val project: Project) : JPanel() { ActionUtil.performActionDumbAwareWithCallbacks(logInToSourcegraphAction, event) } } - return functionGithub } private fun createPanelWithSignInWithAnEnterpriseInstance(): JPanel { @@ -139,11 +122,9 @@ class SignInWithSourcegraphPanel(private val project: Project) : JPanel() { companion object { private const val PADDING = 20 - // 10 here is the default padding from the styles of the h2 and we want to make the whole - // padding - // to be 20, that's why we need the difference between our PADDING and the default padding of - // the - // h2 + // 10 here is the default padding from the styles of the h2 + // and we want to make the whole padding to be 20, that's why + // we need the difference between our PADDING and the default padding of the h2 private const val ADDITIONAL_PADDING_FOR_HEADER = PADDING - 10 } } diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodyOnboardingGuidancePanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodyOnboardingGuidancePanel.kt index 77804500fa..68d86493de 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodyOnboardingGuidancePanel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodyOnboardingGuidancePanel.kt @@ -1,6 +1,5 @@ package com.sourcegraph.cody.chat.ui -import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.project.Project import com.intellij.ui.ColorUtil import com.intellij.ui.JBColor @@ -80,7 +79,6 @@ class CodyOnboardingGuidancePanel(val project: Project) : JPanel() { private fun createGetStartedButton(): JPanel { val buttonPanel = JPanel(BorderLayout()) - mainButton.putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) buttonPanel.add(mainButton, BorderLayout.NORTH) buttonPanel.border = BorderFactory.createEmptyBorder(PADDING, 0, 0, 0) return buttonPanel diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/LlmDropdown.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/LlmDropdown.kt index 1aa9856ade..f9d8312275 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/LlmDropdown.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/ui/LlmDropdown.kt @@ -49,14 +49,19 @@ class LlmDropdown( private fun updateModelsInUI(models: List) { if (project.isDisposed) return - models.filterNot { it.deprecated }.sortedBy { it.codyProOnly }.forEach(::addItem) + val availableModels = models.filterNot { it.deprecated } + availableModels.sortedBy { it.codyProOnly }.forEach(::addItem) val selectedFromState = chatModelProviderFromState val selectedFromHistory = HistoryService.getInstance(project).getDefaultLlm() + val selectedModel = + availableModels.find { it.model == selectedFromState?.model } + ?: availableModels.find { it.model == selectedFromHistory?.model } + selectedItem = - models.find { it.model == selectedFromState?.model && !it.deprecated } - ?: models.find { it.model == selectedFromHistory?.model && !it.deprecated } - ?: models.find { it.default } + if (selectedModel?.codyProOnly == true && isCurrentUserFree()) + availableModels.find { it.default } + else selectedModel val isEnterpriseAccount = CodyAuthenticationManager.getInstance(project).account?.isEnterpriseAccount() ?: false @@ -91,11 +96,10 @@ class LlmDropdown( } } - fun isCurrentUserFree(): Boolean { - return CodyAuthenticationManager.getInstance(project) - .getActiveAccountTier() - .getNow(AccountTier.DOTCOM_FREE) === AccountTier.DOTCOM_FREE - } + fun isCurrentUserFree(): Boolean = + CodyAuthenticationManager.getInstance(project) + .getActiveAccountTier() + .getNow(AccountTier.DOTCOM_FREE) == AccountTier.DOTCOM_FREE @RequiresEdt fun updateAfterFirstMessage() { diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/UIComponents.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/UIComponents.kt index 80d659fbfe..8244da8b0d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/UIComponents.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/ui/UIComponents.kt @@ -22,6 +22,7 @@ object UIComponents { @JvmStatic fun createMainButton(text: String, icon: Icon): JButton { val button = createMainButton(text) + button.putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) button.icon = icon return button } diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountListModel.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountListModel.kt index 7a2f279bae..11f23f16e8 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountListModel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountListModel.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.ui.JBPopupMenu import com.intellij.ui.awt.RelativePoint import com.sourcegraph.cody.auth.ui.AccountsListModel import com.sourcegraph.cody.auth.ui.AccountsListModelBase +import com.sourcegraph.cody.telemetry.TelemetryV2 import javax.swing.JComponent class CodyAccountListModel(private val project: Project) : @@ -61,11 +62,15 @@ class CodyAccountListModel(private val project: Project) : token: String, id: String ) { + TelemetryV2.sendTelemetryEvent(project, "auth.signin.token", "clicked") + val account = CodyAccount(login, displayName, server, id) if (accountsListModel.isEmpty) { activeAccount = account } - accountsListModel.add(account) + if (!accountsListModel.toList().contains(account)) { + accountsListModel.add(account) + } newCredentials[account] = token notifyCredentialsChanged(account) } diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt index 94068db04b..7d44c3fbf7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt @@ -156,6 +156,8 @@ class CodyAuthenticationManager(val project: Project) : isTokenInvalidFuture.complete(error.cause?.message == UNAUTHORIZED_ERROR_MESSAGE) null } + } else { + isTokenInvalidFuture.complete(true) } return authenticationState diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyKeymapExtension.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyKeymapExtension.kt new file mode 100644 index 0000000000..7bf0c101f5 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyKeymapExtension.kt @@ -0,0 +1,28 @@ +package com.sourcegraph.cody.config + +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.keymap.KeymapExtension +import com.intellij.openapi.keymap.KeymapGroup +import com.intellij.openapi.keymap.KeymapGroupFactory +import com.intellij.openapi.keymap.impl.ui.ActionsTreeUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Condition +import com.sourcegraph.common.CodyBundle + +class CodyKeymapExtension : KeymapExtension { + override fun createGroup(filtered: Condition?, project: Project?): KeymapGroup? { + val result = + KeymapGroupFactory.getInstance().createGroup(CodyBundle.getString("cody.plugin-name")) + val actions = ActionsTreeUtil.getActions("Cody.AllActions").toList() + actions.filterIsInstance().forEach { actionGroup -> + val keymapGroup = KeymapGroupFactory.getInstance().createGroup(actionGroup.templateText) + actionGroup.getChildren(null).forEach { + ActionsTreeUtil.addAction(keymapGroup, it, filtered, true) + } + result.addGroup(keymapGroup) + } + + return result + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyPersistentAccountsHost.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyPersistentAccountsHost.kt index 93ce80595e..c3a8a4d585 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/CodyPersistentAccountsHost.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyPersistentAccountsHost.kt @@ -1,6 +1,7 @@ package com.sourcegraph.cody.config import com.intellij.openapi.project.Project +import com.sourcegraph.cody.telemetry.TelemetryV2 class CodyPersistentAccountsHost(private val project: Project) : CodyAccountsHost { override fun addAccount( @@ -10,6 +11,8 @@ class CodyPersistentAccountsHost(private val project: Project) : CodyAccountsHos token: String, id: String ) { + TelemetryV2.sendTelemetryEvent(project, "auth.signin.token", "clicked") + val codyAccount = CodyAccount(login, displayName, server, id) val authManager = CodyAuthenticationManager.getInstance(project) authManager.updateAccountToken(codyAccount, token) diff --git a/src/main/kotlin/com/sourcegraph/cody/config/LogInToSourcegraphAction.kt b/src/main/kotlin/com/sourcegraph/cody/config/LogInToSourcegraphAction.kt index 2bf7ed6503..29bacec5d3 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/LogInToSourcegraphAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/LogInToSourcegraphAction.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.util.ui.JBUI import com.sourcegraph.cody.api.SourcegraphApiRequestExecutor import com.sourcegraph.cody.auth.SsoAuthMethod +import com.sourcegraph.cody.telemetry.TelemetryV2 import com.sourcegraph.common.ui.DumbAwareEDTAction import java.awt.Component import javax.swing.Action @@ -18,6 +19,8 @@ class LogInToSourcegraphAction : BaseAddAccountWithTokenAction() { get() = SourcegraphServerPath.DEFAULT_HOST override fun actionPerformed(e: AnActionEvent) { + e.project?.let { TelemetryV2.sendTelemetryEvent(it, "auth.login", "clicked") } + val accountsHost = getCodyAccountsHost(e) ?: return val authMethod: SsoAuthMethod = try { @@ -42,6 +45,8 @@ class AddCodyEnterpriseAccountAction : BaseAddAccountWithTokenAction() { get() = "" override fun actionPerformed(e: AnActionEvent) { + e.project?.let { TelemetryV2.sendTelemetryEvent(it, "auth.login", "clicked") } + val accountsHost = getCodyAccountsHost(e) ?: return val dialog = newAddAccountDialog(e.project, e.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT)) 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 75464847e9..17e11081cd 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/sessions/FixupSession.kt @@ -94,8 +94,11 @@ abstract class FixupSession( } init { - document.addDocumentListener(documentListener, /* parentDisposable= */ this) - Disposer.register(controller, this) + // There is a race condition here, but it keeps us from leaking 'this' in the constructor. + ApplicationManager.getApplication().invokeAndWait { + Disposer.register(controller, this) + document.addDocumentListener(documentListener, /* parentDisposable= */ this) + } triggerFixupAsync() } @@ -466,10 +469,6 @@ abstract class FixupSession( return lensGroup?.isErrorGroup == true } - fun hasAcceptLensBeenShown(): Boolean { - return documentListener.isAcceptLensGroupShown.get() - } - private fun publishProgress(topic: Topic) { ApplicationManager.getApplication().invokeLater { project.messageBus.syncPublisher(topic).afterAction() 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 8e049cafd9..1ac22ceb1b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensGroupFactory.kt @@ -40,6 +40,7 @@ class LensGroupFactory(val session: FixupSession) { fun createErrorGroup(tooltip: String, isDocumentCode: Boolean = false): LensWidgetGroup { return LensWidgetGroup(session, session.editor).apply { + errorMessage = tooltip addLogo(this) addErrorIcon(this) val verb = if (isDocumentCode) "document" else "edit" 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 d4f233b76e..e0c24083cd 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt @@ -57,6 +57,9 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : val widgets = mutableListOf() + // Set on Error groups. + var errorMessage: String? = null + private val mouseClickListener = object : EditorMouseListener { override fun mouseClicked(e: EditorMouseEvent) { @@ -235,7 +238,7 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : for (widget in widgets) { val widgetWidth = widget.calcWidthInPixels(fontMetrics) val rightEdgeX = currentX + widgetWidth - if (x >= currentX && x <= rightEdgeX) { // In widget's bounds? + if (x in currentX..rightEdgeX) { // In widget's bounds? return widget } currentX = rightEdgeX @@ -392,7 +395,7 @@ class LensWidgetGroup(val session: FixupSession, parentComponent: Editor) : // was found empirically and seems to work well for all font sizes. private const val INLAY_HEIGHT_SCALE_FACTOR = 1.2 - // Flag to force recomputation of left margin. + // Flag to force recalculation of left margin. private const val RECOMPUTE = -1 } } diff --git a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt index ea57489478..867d1d6caf 100644 --- a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt +++ b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt @@ -13,6 +13,7 @@ import com.sourcegraph.cody.listeners.CodyDocumentListener import com.sourcegraph.cody.listeners.CodyFocusChangeListener import com.sourcegraph.cody.listeners.CodySelectionListener import com.sourcegraph.cody.statusbar.CodyStatusService +import com.sourcegraph.cody.telemetry.TelemetryV2 import com.sourcegraph.config.CodyAuthNotificationActivity import com.sourcegraph.config.ConfigUtil import com.sourcegraph.telemetry.TelemetryInitializerActivity @@ -52,5 +53,7 @@ class PostStartupActivity : StartupActivity.DumbAware { multicaster.addCaretListener(CodyCaretListener(project), disposable) multicaster.addSelectionListener(CodySelectionListener(project), disposable) multicaster.addDocumentListener(CodyDocumentListener(project), disposable) + + TelemetryV2.sendTelemetryEvent(project, "cody.extension", "started") } } diff --git a/src/main/kotlin/com/sourcegraph/cody/util/TestFile.kt b/src/main/kotlin/com/sourcegraph/cody/util/TestFile.kt new file mode 100644 index 0000000000..8a4ee75b7c --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/util/TestFile.kt @@ -0,0 +1,5 @@ +package com.sourcegraph.cody.util + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class TestFile(val value: String) diff --git a/src/main/resources/CodyBundle.properties b/src/main/resources/CodyBundle.properties index af4d604bb6..37dbd3e22c 100644 --- a/src/main/resources/CodyBundle.properties +++ b/src/main/resources/CodyBundle.properties @@ -1,3 +1,4 @@ +cody.plugin-name=Cody: AI Coding Assistant with Autocomplete & Chat status-widget.warning.pro.dialog-title=Thank you for using Cody so heavily today! status-widget.warning.pro.content=\ \ @@ -75,7 +76,6 @@ UpgradeToCodyProNotification.content.upgrade=\ (Already upgraded to Pro? Restart your IDE for changes to take effect)\ UpgradeToCodyProNotification.content.explain=To ensure that Cody can stay operational for all Cody users, please come back tomorrow for more chats, commands, and autocompletes. -context-panel.button.edit-repositories=Edit Remote Repositories context-panel.button.reindex=Reindex Local Project context-panel.button.help=Help context-panel.in-progress=Running Cody 'Keyword Search' indexer... @@ -136,55 +136,6 @@ PromptPanel.ask-cody.message=Message (type @ to include specific files as contex PromptPanel.ask-cody.follow-up-message=Follow-up message (type @ to include specific files as context) LlmDropdown.disabled.text=Start a new chat to change the model -# Actions Groups -group.SourcegraphEditor.text=Sourcegraph -group.CodyStatusBarActions.text=Cody -group.CodyEditorActions.text=Cody -group.InternalsStatusBarActions.text=?? Internals - -# Authentication Actions -action.Cody.Accounts.LogInToSourcegraphAction.text=Log In to Sourcegraph -action.Cody.Accounts.AddCodyEnterpriseAccount.text=Log In with Token to Sourcegraph Enterprise - -# Sourcegraph Actions -action.sourcegraph.openFile.text=Open Selection in Sourcegraph Web -action.sourcegraph.openFile.description=Open selection in Sourcegraph Web -action.sourcegraph.searchSelection.text=Search Selection on Sourcegraph Web -action.sourcegraph.searchSelection.description=Search selection on Sourcegraph web -action.sourcegraph.searchRepository.text=Search Selection in Repository on Sourcegraph Web -action.sourcegraph.searchRepository.description=Search selection in repository on Sourcegraph web -action.sourcegraph.copy.text=Copy Sourcegraph File Link -action.sourcegraph.copy.description=Copy Sourcegraph file link -action.com.sourcegraph.website.OpenRevisionAction.text=Open Revision Diff in Sourcegraph Web -action.sourcegraph.openFindPopup.text=Find with Sourcegraph... -action.sourcegraph.openFindPopup.description=Search all your repos on Sourcegraph -action.sourcegraph.login.text=Log in to Sourcegraph -action.sourcegraph.login.description=Log in to Sourcegraph -action.sourcegraph.disabled.description=Log in to Sourcegraph to enable Cody features - -# Chat Actions -action.cody.openChat.text=Open Chat -action.cody.newChat.text=New Chat -action.cody.newChat.description=New chat -action.cody.exportChats.text=Export All Chats As JSON -action.cody.exportChats.description=Export all chats as JSON -action.cody.command.Explain.text=Explain Code -action.cody.command.Smell.text=Find Code Smells - -# Inline Edit Actions -action.cody.enableInlineEditsActions.text=Enable Inline Edits -action.cody.editCodeAction.text=Edit Code... -action.cody.documentCodeAction.text=Document Code -action.cody.editShowDiffAction.text=Show Diff -action.cody.testCodeAction.text=Generate Unit Tests - -# Autocomplete Actions -action.cody.acceptAutocompleteAction.text=Accept Autocomplete Suggestion -action.cody.cycleForwardAutocompleteAction.text=Cycle Forward Autocomplete Suggestion -action.cody.cycleBackAutocompleteAction.text=Cycle Backwards Autocomplete Suggestion -action.cody.disposeInlays.text=Hide Completions -action.cody.triggerAutocomplete.text=Autocomplete - # GotItTooltip gotit.autocomplete.header=Your first Cody Autocomplete gotit.autocomplete.message=This is how Cody displays autocomplete suggestions.
Press {0} to insert it into the editor.
Press {1} and {2} to cycle through alternatives. diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 76001c7c3c..66bde0b7f6 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -98,6 +98,8 @@ order="first, before commitCompletion"/> + + @@ -111,226 +113,310 @@ result in the actual keybindings switching to command/meta and/or shift. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + class="com.sourcegraph.cody.statusbar.CodyStatusBarActionGroup" + text="Cody" + description="Cody status bar actions"> - - - - - - + + + + + + +