From d67dbda64bb6bfbe22db11b13abb799a3fc7b854 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Wed, 18 Sep 2024 16:09:51 +0200 Subject: [PATCH 1/8] Initial implementation of lenses using codeVision --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 44 +- .../cody/util/CodyIntegrationTextFixture.kt | 12 +- .../sourcegraph/cody/agent/CodyAgentClient.kt | 2 +- .../cody/edit/EditCommandPrompt.kt | 8 - .../sourcegraph/cody/edit/LensesService.kt | 183 --------- .../edit/lenses/EditCodeVisionProvider.kt | 147 +++++++ .../cody/edit/lenses/LensesService.kt | 81 ++++ .../actions}/EditAcceptAction.kt | 2 +- .../actions}/EditCancelAction.kt | 2 +- .../actions}/EditRetryAction.kt | 2 +- .../actions}/EditShowDiffAction.kt | 2 +- .../actions}/EditUndoAction.kt | 2 +- .../actions}/LensEditAction.kt | 4 +- .../providers/EditAcceptCodeVisionProvider.kt | 15 + .../providers/EditCancelCodeVisionProvider.kt | 14 + .../providers/EditDiffCodeVisionProvider.kt | 12 + .../providers/EditRetryCodeVisionProvider.kt | 13 + .../providers/EditUndoCodeVisionProvider.kt | 14 + .../EditWorkingCodeVisionProvider.kt | 13 + .../cody/edit/widget/LabelHighlight.kt | 31 -- .../cody/edit/widget/LensAction.kt | 120 ------ .../cody/edit/widget/LensHotkey.kt | 43 -- .../sourcegraph/cody/edit/widget/LensIcon.kt | 48 --- .../sourcegraph/cody/edit/widget/LensLabel.kt | 36 -- .../cody/edit/widget/LensSpinner.kt | 15 - .../cody/edit/widget/LensWidget.kt | 110 ----- .../cody/edit/widget/LensWidgetGroup.kt | 381 ------------------ .../com/sourcegraph/cody/ui/FrameMover.kt | 244 ----------- src/main/resources/META-INF/plugin.xml | 30 +- 29 files changed, 373 insertions(+), 1257 deletions(-) delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/LensesService.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt rename src/main/kotlin/com/sourcegraph/cody/edit/{actions/lenses => lenses/actions}/EditAcceptAction.kt (89%) rename src/main/kotlin/com/sourcegraph/cody/edit/{actions/lenses => lenses/actions}/EditCancelAction.kt (89%) rename src/main/kotlin/com/sourcegraph/cody/edit/{actions/lenses => lenses/actions}/EditRetryAction.kt (92%) rename src/main/kotlin/com/sourcegraph/cody/edit/{actions/lenses => lenses/actions}/EditShowDiffAction.kt (96%) rename src/main/kotlin/com/sourcegraph/cody/edit/{actions/lenses => lenses/actions}/EditUndoAction.kt (88%) rename src/main/kotlin/com/sourcegraph/cody/edit/{actions/lenses => lenses/actions}/LensEditAction.kt (95%) create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditAcceptCodeVisionProvider.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditCancelCodeVisionProvider.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditDiffCodeVisionProvider.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditRetryCodeVisionProvider.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditUndoCodeVisionProvider.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditWorkingCodeVisionProvider.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LabelHighlight.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensIcon.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt delete mode 100644 src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index d69807c40e..271b021b6d 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -1,15 +1,16 @@ package com.sourcegraph.cody.edit import com.sourcegraph.cody.edit.actions.DocumentCodeAction -import com.sourcegraph.cody.edit.actions.lenses.EditAcceptAction -import com.sourcegraph.cody.edit.actions.lenses.EditCancelAction -import com.sourcegraph.cody.edit.actions.lenses.EditUndoAction -import com.sourcegraph.cody.edit.widget.LensAction -import com.sourcegraph.cody.edit.widget.LensHotkey -import com.sourcegraph.cody.edit.widget.LensIcon -import com.sourcegraph.cody.edit.widget.LensLabel -import com.sourcegraph.cody.edit.widget.LensSpinner -import com.sourcegraph.cody.edit.widget.LensWidgetGroup +import com.sourcegraph.cody.edit.lenses.LensesService +import com.sourcegraph.cody.edit.lenses.actions.EditAcceptAction +import com.sourcegraph.cody.edit.lenses.actions.EditCancelAction +import com.sourcegraph.cody.edit.lenses.actions.EditUndoAction +import com.sourcegraph.cody.edit.lenses.widget.LensActionWidget +import com.sourcegraph.cody.edit.lenses.widget.LensHotkeyWidget +import com.sourcegraph.cody.edit.lenses.widget.LensIconWidget +import com.sourcegraph.cody.edit.lenses.widget.LensLabelWidget +import com.sourcegraph.cody.edit.lenses.widget.LensSpinnerWidget +import com.sourcegraph.cody.edit.lenses.widget.LensWidgetGroup import com.sourcegraph.cody.util.CodyIntegrationTextFixture import com.sourcegraph.cody.util.CustomJunitClassRunner import org.hamcrest.MatcherAssert.assertThat @@ -37,20 +38,21 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { val theWidgets = codeLensGroup!!.widgets assertEquals("Lens group should have 9 widgets", 9, theWidgets.size) - assertTrue("Zeroth lens group should be an icon", theWidgets[0] is LensIcon) + assertTrue("Zeroth lens group should be an icon", theWidgets[0] is LensIconWidget) assertTrue( - "First lens group is space separator label", (theWidgets[1] as LensLabel).text == " ") - assertTrue("Second lens group is a spinner", theWidgets[2] is LensSpinner) + "First lens group is space separator label", (theWidgets[1] as LensLabelWidget).text == " ") + assertTrue("Second lens group is a spinner", theWidgets[2] is LensSpinnerWidget) assertTrue( - "Third lens group is space separator label", (theWidgets[3] as LensLabel).text == " ") + "Third lens group is space separator label", (theWidgets[3] as LensLabelWidget).text == " ") assertTrue( "Fourth lens group is a description label", - (theWidgets[4] as LensAction).text == " Cody is working...") + (theWidgets[4] as LensActionWidget).text == " Cody is working...") assertTrue( "Fifth lens group is separator label", - (theWidgets[5] as LensLabel).text == LensesService.SEPARATOR) - assertTrue("Sixth lens group should be an action", theWidgets[6] is LensAction) - assertTrue("Seventh lens group should be a label with a hotkey", theWidgets[7] is LensHotkey) + (theWidgets[5] as LensLabelWidget).text == LensesService.SEPARATOR) + assertTrue("Sixth lens group should be an action", theWidgets[6] is LensActionWidget) + assertTrue( + "Seventh lens group should be a label with a hotkey", theWidgets[7] is LensHotkeyWidget) runLensAction(codeLensGroup, EditCancelAction.ID) assertNoInlayShown() @@ -74,10 +76,14 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { assertTrue("Lens group should have at least 4 widgets", widgets.size >= 4) assertNotNull( "Lens group should contain Accept action", - widgets.find { widget -> widget is LensAction && widget.actionId == EditAcceptAction.ID }) + widgets.find { widget -> + widget is LensActionWidget && widget.actionId == EditAcceptAction.ID + }) assertNotNull( "Lens group should contain Show Undo action", - widgets.find { widget -> widget is LensAction && widget.actionId == EditUndoAction.ID }) + widgets.find { widget -> + widget is LensActionWidget && widget.actionId == EditUndoAction.ID + }) // Make sure a doc comment was inserted. assertTrue(hasJavadocComment(myFixture.editor.document.text)) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt index 01ef0fab47..31d44ca85d 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt @@ -22,10 +22,10 @@ import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.protocol_generated.ProtocolCodeLens import com.sourcegraph.cody.config.CodyPersistentAccountsHost import com.sourcegraph.cody.config.SourcegraphServerPath -import com.sourcegraph.cody.edit.LensListener -import com.sourcegraph.cody.edit.LensesService -import com.sourcegraph.cody.edit.widget.LensAction -import com.sourcegraph.cody.edit.widget.LensWidgetGroup +import com.sourcegraph.cody.edit.lenses.LensListener +import com.sourcegraph.cody.edit.lenses.LensesService +import com.sourcegraph.cody.edit.lenses.widget.LensActionWidget +import com.sourcegraph.cody.edit.lenses.widget.LensWidgetGroup import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern @@ -257,8 +257,8 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener { lensSubscribers.add(check to future) runInEdtAndWait { - val action: LensAction? = - lensWidgetGroup.widgets.filterIsInstance().find { + val action: LensActionWidget? = + lensWidgetGroup.widgets.filterIsInstance().find { it.actionId == actionLensId } PlatformTestUtil.dispatchAllEventsInIdeEventQueue() diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt index 40b338a09e..5173a15d2f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt @@ -22,7 +22,7 @@ import com.sourcegraph.cody.agent.protocol_generated.TextDocument_ShowParams import com.sourcegraph.cody.agent.protocol_generated.UntitledTextDocument import com.sourcegraph.cody.agent.protocol_generated.WorkspaceEditParams import com.sourcegraph.cody.edit.EditService -import com.sourcegraph.cody.edit.LensesService +import com.sourcegraph.cody.edit.lenses.LensesService import com.sourcegraph.cody.error.CodyConsole import com.sourcegraph.cody.ignore.IgnoreOracle import com.sourcegraph.cody.ui.web.NativeWebviewProvider diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt index 2b59897e16..eb4182cd16 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt @@ -457,14 +457,6 @@ class EditCommandPrompt( val commandPrompt = EDIT_COMMAND_PROMPT_KEY.get(project) return commandPrompt?.popup?.isVisible == true } - - /** Returns a compact symbol representation of the action's keyboard shortcut, if any. */ - @JvmStatic - fun getShortcutDisplayString(actionId: String): String? { - return KeymapManager.getInstance().activeKeymap.getShortcuts(actionId).firstOrNull()?.let { - KeymapUtil.getShortcutText(it) - } - } } override fun getData(dataId: String): Any? { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/LensesService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/LensesService.kt deleted file mode 100644 index 6d256546f3..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/LensesService.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.sourcegraph.cody.edit - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.sourcegraph.cody.Icons -import com.sourcegraph.cody.agent.protocol_generated.ProtocolCodeLens -import com.sourcegraph.cody.edit.widget.LensAction -import com.sourcegraph.cody.edit.widget.LensHotkey -import com.sourcegraph.cody.edit.widget.LensIcon -import com.sourcegraph.cody.edit.widget.LensLabel -import com.sourcegraph.cody.edit.widget.LensSpinner -import com.sourcegraph.cody.edit.widget.LensWidgetGroup -import com.sourcegraph.utils.CodyEditorUtil -import java.awt.Point -import javax.swing.Icon - -typealias Uri = String - -typealias TaskId = String - -interface LensListener { - fun onLensesUpdate(lensWidgetGroup: LensWidgetGroup?, codeLenses: List) -} - -@Service(Service.Level.PROJECT) -class LensesService(val project: Project) { - private var lensGroups = mutableMapOf>() - - private val listeners = mutableListOf() - - fun addListener(listener: LensListener) { - listeners.add(listener) - } - - fun removeListener(listener: LensListener) { - listeners.remove(listener) - } - - fun getTaskIdsOfFirstVisibleLens(editor: Editor): TaskId? { - val visibleArea = editor.scrollingModel.visibleArea - val visibleInlays = - editor.inlayModel.getBlockElementsInRange( - editor.visualPositionToOffset(editor.xyToVisualPosition(visibleArea.location)), - editor.visualPositionToOffset( - editor.xyToVisualPosition( - Point(visibleArea.x + visibleArea.width, visibleArea.y + visibleArea.height)))) - - return lensGroups.values - .flatMap { param -> param.toList() } - .find { (_, lensGroup) -> visibleInlays.any { it == lensGroup.inlay } } - ?.first - } - - @RequiresEdt - fun updateLenses(uri: String, codeLens: List) { - val vf = CodyEditorUtil.findFileOrScratch(project, uri) ?: return - val fileDesc = OpenFileDescriptor(project, vf) - val editor = - FileEditorManager.getInstance(project).openTextEditor(fileDesc, /* focusEditor = */ false) - ?: return - - val ranges = codeLens.groupBy { it.range }.keys.sortedBy { it.start.line } - ranges.zipWithNext { r1, r2 -> - if (r1.end.line > r2.start.line) - throw Exception("Lens ranges $r1 and $r2 for file $uri cannot overlap") - } - - val newLensGroups = - codeLens - .groupBy { it.range } - .map { (range, codeLensesModels) -> - val taskId = codeLensesModels.firstNotNullOf { getTaskId(it) } - taskId to (range to createLensGroup(editor, codeLensesModels)) - } - .toMap() - - lensGroups[uri]?.values?.forEach { Disposer.dispose(it) } - newLensGroups.forEach { (taskId, rangeAndLensGroup) -> - val (range, lensGroup) = rangeAndLensGroup - val isNewTask = !lensGroups.containsKey(taskId) - lensGroup.show(range, shouldScrollToLens = isNewTask) - } - - newLensGroups.forEach { (taskId, rangeAndLensGroup) -> - val lenses = codeLens.filter { getTaskId(it) == taskId } - listeners.forEach { it.onLensesUpdate(rangeAndLensGroup.second, lenses) } - } - if (newLensGroups.isEmpty()) { - listeners.forEach { it.onLensesUpdate(null, emptyList()) } - } - - lensGroups[uri] = newLensGroups.mapValues { it.value.second } - } - - private fun getTaskId(codeLens: ProtocolCodeLens): String? { - return (codeLens.command?.arguments?.firstOrNull() as com.google.gson.JsonPrimitive).asString - } - - private fun createLensGroup(editor: Editor, codeLens: List): LensWidgetGroup { - return LensWidgetGroup(editor).apply { - addLogo(this) - - codeLens.forEach { lens -> - val title = lens.command?.title?.text - val command = lens.command?.command - if (title != null && command != null) { - if (lens.command.title.icons.any { it.value == SPINNER_ICON }) { - addSpinner(this) - } - - if (command === FOCUS_COMMAND) { - addLabel(this, title) - } else { - val taskId = - (lens.command.arguments?.firstOrNull() as com.google.gson.JsonPrimitive).asString - addAction(this, title, command, taskId) - } - - addSeparator(this) - } - } - } - } - - private fun addSeparator(group: LensWidgetGroup) { - group.addWidget(LensLabel(group, SEPARATOR)) - } - - private fun addLabel( - group: LensWidgetGroup, - label: String, - ): LensLabel { - return LensLabel(group, label).apply { group.addWidget(this) } - } - - private fun addSpinner(group: LensWidgetGroup) { - group.addWidget(LensSpinner(group, Icons.StatusBar.CompletionInProgress)) - addSpacer(group) - } - - private fun addLogo(group: LensWidgetGroup) { - addIcon(group, Icons.StatusBar.CodyAvailable) - } - - private fun addSpacer(group: LensWidgetGroup) { - addLabel(group, ICON_SPACER) - } - - private fun addAction(group: LensWidgetGroup, label: String, actionId: String, taskId: String?) { - group.addWidget(LensAction(group, label, actionId, taskId)) - - val hotkey = EditCommandPrompt.getShortcutDisplayString(actionId) - if (!hotkey.isNullOrEmpty()) { - group.addWidget(LensHotkey(group, hotkey)) - } - } - - private fun addErrorIcon(group: LensWidgetGroup) { - addIcon(group, Icons.Edit.Error) - } - - private fun addIcon(group: LensWidgetGroup, icon: Icon) { - group.addWidget(LensIcon(group, icon)) - addSpacer(group) - } - - companion object { - private const val SPINNER_ICON = "$(sync~spin)" - const val ICON_SPACER = " " - const val SEPARATOR = " ∣ " - const val FOCUS_COMMAND = "cody.chat.focus" - - fun getInstance(project: Project): LensesService { - return project.service() - } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt new file mode 100644 index 0000000000..9ab765d180 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt @@ -0,0 +1,147 @@ +package com.sourcegraph.cody.edit.lenses + +import com.intellij.codeInsight.codeVision.CodeVisionAnchorKind +import com.intellij.codeInsight.codeVision.CodeVisionProvider +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.intellij.codeInsight.codeVision.CodeVisionState +import com.intellij.codeInsight.codeVision.CodeVisionState.Companion.READY_EMPTY +import com.intellij.codeInsight.codeVision.ui.model.ClickableRichTextCodeVisionEntry +import com.intellij.codeInsight.codeVision.ui.model.richText.RichText +import com.intellij.icons.AllIcons +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.application.runReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.keymap.KeymapManager +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.ui.JBColor +import com.intellij.ui.SimpleTextAttributes +import com.sourcegraph.cody.Icons +import com.sourcegraph.cody.agent.protocol_generated.ProtocolCommand +import com.sourcegraph.cody.edit.lenses.actions.LensEditAction.Companion.TASK_ID_KEY +import com.sourcegraph.cody.edit.lenses.providers.EditAcceptCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditCancelCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditDiffCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditRetryCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditUndoCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditWorkingCodeVisionProvider +import com.sourcegraph.utils.CodyEditorUtil +import java.awt.event.MouseEvent +import javax.swing.Icon + +abstract class EditCodeVisionProviderMetadata { + abstract val ordering: CodeVisionRelativeOrdering + abstract val command: String + open val textColor: JBColor = JBColor.BLACK + + val id: String + get() = "EditCodeVisionProvider-${command}" + + fun showAfter(providerCompanion: EditCodeVisionProviderMetadata): CodeVisionRelativeOrdering { + return CodeVisionRelativeOrdering.CodeVisionRelativeOrderingAfter(providerCompanion.id) + } +} + +abstract class EditCodeVisionProvider(private val metadata: EditCodeVisionProviderMetadata) : + CodeVisionProvider { + override val id: String = metadata.id + override val groupId: String = "EditCodeVisionProvider" + override val name: String = "Cody Edit Lenses" + override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Top + override val relativeOrderings: List = listOf(metadata.ordering) + + override fun precomputeOnUiThread(editor: Editor) {} + + private fun getIcon(iconId: String): Icon? { + return when (iconId) { + "$(cody-logo)" -> Icons.StatusBar.CodyAvailable + "$(sync~spin)" -> Icons.StatusBar.CompletionInProgress + "$(warning)" -> AllIcons.General.Warning + else -> null + } + } + + private fun getActionRichText(cmd: ProtocolCommand): RichText { + return RichText().also { + it.append( + cmd.title.text, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, metadata.textColor)) + + val shortcuts = KeymapManager.getInstance().activeKeymap.getShortcuts(cmd.command) + shortcuts.firstOrNull()?.let { shortcut -> + val hotkey = KeymapUtil.getShortcutText(shortcut) + it.append(" $hotkey", SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, JBColor.GRAY)) + } + + it.append(" |", SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, JBColor.GRAY)) + } + } + + override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { + return runReadAction { + val project = editor.project ?: return@runReadAction READY_EMPTY + + val codeVisionEntries = + LensesService.getInstance(project).getLenses(editor).mapNotNull { codeLens -> + val cmd = codeLens.command + if (cmd == null || cmd.command != metadata.command) null + else { + val richText = getActionRichText(cmd) + val icon = cmd.title.icons.firstOrNull()?.value?.let { getIcon(it) } + val textRange = CodyEditorUtil.getTextRange(editor.document, codeLens.range) + val onClick = { event: MouseEvent?, editor: Editor -> + triggerAction(cmd, event, editor) + } + val entry = + ClickableRichTextCodeVisionEntry( + id, richText, onClick, icon, "", richText.text, emptyList()) + textRange to entry + } + } + + return@runReadAction CodeVisionState.Ready(codeVisionEntries) + } + } + + private fun createDataContext(editor: Editor, taskId: String?): DataContext { + return DataContext { dataId -> + when (dataId) { + PlatformDataKeys.CONTEXT_COMPONENT.name -> this + PlatformDataKeys.EDITOR.name -> editor + PlatformDataKeys.PROJECT.name -> editor.project + TASK_ID_KEY.name -> taskId + else -> null + } + } + } + + private fun triggerAction(cmd: ProtocolCommand, event: MouseEvent?, editor: Editor) { + val action = ActionManager.getInstance().getAction(cmd.command) + if (action != null) { + val taskId = (cmd.arguments?.firstOrNull() as com.google.gson.JsonPrimitive).asString + val dataContext = createDataContext(editor, taskId) + val actionEvent = + AnActionEvent( + event, + dataContext, + "", + action.templatePresentation.clone(), + ActionManager.getInstance(), + 0) + action.actionPerformed(actionEvent) + } + } + + companion object { + fun allEditProviders(): List { + return listOf( + EditAcceptCodeVisionProvider, + EditCancelCodeVisionProvider, + EditDiffCodeVisionProvider, + EditWorkingCodeVisionProvider, + EditRetryCodeVisionProvider, + EditUndoCodeVisionProvider) + } + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt new file mode 100644 index 0000000000..54a4fe2d92 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt @@ -0,0 +1,81 @@ +package com.sourcegraph.cody.edit.lenses + +import com.intellij.codeInsight.codeVision.CodeVisionHost +import com.intellij.codeInsight.codeVision.CodeVisionInitializer +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.sourcegraph.cody.agent.protocol_generated.ProtocolCodeLens +import java.awt.Point +import java.net.URI + +typealias TaskId = String + +interface LensListener { + fun onLensesUpdate(uri: URI, codeLenses: List) +} + +@Service(Service.Level.PROJECT) +class LensesService(val project: Project) { + @Volatile private var lensGroups = mutableMapOf>() + + private val listeners = mutableListOf() + + fun getTaskIdsOfFirstVisibleLens(editor: Editor): TaskId? { + val lenses = getLenses(editor).sortedBy { it.range.start.line } + val visibleArea = editor.scrollingModel.visibleArea + val startPosition = editor.xyToVisualPosition(visibleArea.location) + val endPosition = + editor.xyToVisualPosition( + Point(visibleArea.x + visibleArea.width, visibleArea.y + visibleArea.height)) + val cmd = lenses.find { it.range.start.line in (startPosition.line..endPosition.line) }?.command + val taskId = (cmd?.arguments?.firstOrNull() as com.google.gson.JsonPrimitive).asString + return taskId + } + + fun addListener(listener: LensListener) { + listeners.add(listener) + } + + fun removeListener(listener: LensListener) { + listeners.remove(listener) + } + + fun updateLenses(uriString: String, codeLens: List) { + val uri = URI.create(uriString) ?: return + synchronized(this) { lensGroups[uri] = codeLens } + + listeners.forEach { it.onLensesUpdate(uri, codeLens) } + + runInEdt { + if (project.isDisposed) return@runInEdt + val editor = FileEditorManager.getInstance(project).selectedTextEditor + CodeVisionInitializer.getInstance(project) + .getCodeVisionHost() + .invalidateProvider( + CodeVisionHost.LensInvalidateSignal( + editor, EditCodeVisionProvider.allEditProviders().map { it.id })) + } + } + + fun getLenses(editor: Editor): List { + val document = editor.document + val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return emptyList() + val virtualFile = file.viewProvider.virtualFile + val uri = URI.create(virtualFile.url) ?: return emptyList() + + synchronized(this) { + return lensGroups[uri] ?: emptyList() + } + } + + companion object { + fun getInstance(project: Project): LensesService { + return project.service() + } + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditAcceptAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditAcceptAction.kt similarity index 89% rename from src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditAcceptAction.kt rename to src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditAcceptAction.kt index 48bb23bc79..16028a1397 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditAcceptAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditAcceptAction.kt @@ -1,4 +1,4 @@ -package com.sourcegraph.cody.edit.actions.lenses +package com.sourcegraph.cody.edit.lenses.actions import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.protocol_generated.EditTask_AcceptParams diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditCancelAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditCancelAction.kt similarity index 89% rename from src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditCancelAction.kt rename to src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditCancelAction.kt index ca1b15b998..3f4a656739 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditCancelAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditCancelAction.kt @@ -1,4 +1,4 @@ -package com.sourcegraph.cody.edit.actions.lenses +package com.sourcegraph.cody.edit.lenses.actions import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.protocol_generated.EditTask_CancelParams diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditRetryAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditRetryAction.kt similarity index 92% rename from src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditRetryAction.kt rename to src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditRetryAction.kt index 13631e18b1..0c28135075 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditRetryAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditRetryAction.kt @@ -1,4 +1,4 @@ -package com.sourcegraph.cody.edit.actions.lenses +package com.sourcegraph.cody.edit.lenses.actions import com.intellij.openapi.application.runInEdt import com.sourcegraph.cody.edit.EditCommandPrompt diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditShowDiffAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditShowDiffAction.kt similarity index 96% rename from src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditShowDiffAction.kt rename to src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditShowDiffAction.kt index 9adc90caff..dc123f9ffe 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditShowDiffAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditShowDiffAction.kt @@ -1,4 +1,4 @@ -package com.sourcegraph.cody.edit.actions.lenses +package com.sourcegraph.cody.edit.lenses.actions import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.EditorFactory diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditUndoAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditUndoAction.kt similarity index 88% rename from src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditUndoAction.kt rename to src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditUndoAction.kt index 06d9373dff..608ed3bbdb 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/EditUndoAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/EditUndoAction.kt @@ -1,4 +1,4 @@ -package com.sourcegraph.cody.edit.actions.lenses +package com.sourcegraph.cody.edit.lenses.actions import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.protocol_generated.EditTask_UndoParams diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/LensEditAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/LensEditAction.kt similarity index 95% rename from src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/LensEditAction.kt rename to src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/LensEditAction.kt index 7e5b5d5b02..307062dc37 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/actions/lenses/LensEditAction.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/actions/LensEditAction.kt @@ -1,4 +1,4 @@ -package com.sourcegraph.cody.edit.actions.lenses +package com.sourcegraph.cody.edit.lenses.actions import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -10,7 +10,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.sourcegraph.cody.config.CodyAuthenticationManager -import com.sourcegraph.cody.edit.LensesService +import com.sourcegraph.cody.edit.lenses.LensesService import com.sourcegraph.common.CodyBundle abstract class LensEditAction(val editAction: (Project, AnActionEvent, Editor, String) -> Unit) : diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditAcceptCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditAcceptCodeVisionProvider.kt new file mode 100644 index 0000000000..dff7e3de88 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditAcceptCodeVisionProvider.kt @@ -0,0 +1,15 @@ +package com.sourcegraph.cody.edit.lenses.providers + +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.intellij.ui.JBColor +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProviderMetadata + +class EditAcceptCodeVisionProvider : EditCodeVisionProvider(EditAcceptCodeVisionProvider) { + companion object : EditCodeVisionProviderMetadata() { + override val ordering: CodeVisionRelativeOrdering = + CodeVisionRelativeOrdering.CodeVisionRelativeOrderingFirst + override val command: String = "cody.fixup.codelens.accept" + override val textColor: JBColor = JBColor.GREEN + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditCancelCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditCancelCodeVisionProvider.kt new file mode 100644 index 0000000000..1e964c5e0c --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditCancelCodeVisionProvider.kt @@ -0,0 +1,14 @@ +package com.sourcegraph.cody.edit.lenses.providers + +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.intellij.ui.JBColor +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProviderMetadata + +class EditCancelCodeVisionProvider : EditCodeVisionProvider(EditCancelCodeVisionProvider) { + companion object : EditCodeVisionProviderMetadata() { + override val ordering: CodeVisionRelativeOrdering = showAfter(EditWorkingCodeVisionProvider) + override val command: String = "cody.fixup.codelens.cancel" + override val textColor: JBColor = JBColor.RED + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditDiffCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditDiffCodeVisionProvider.kt new file mode 100644 index 0000000000..20d6996085 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditDiffCodeVisionProvider.kt @@ -0,0 +1,12 @@ +package com.sourcegraph.cody.edit.lenses.providers + +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProviderMetadata + +class EditDiffCodeVisionProvider : EditCodeVisionProvider(EditDiffCodeVisionProvider) { + companion object : EditCodeVisionProviderMetadata() { + override val ordering: CodeVisionRelativeOrdering = showAfter(EditUndoCodeVisionProvider) + override val command: String = "cody.fixup.codelens.diff" + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditRetryCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditRetryCodeVisionProvider.kt new file mode 100644 index 0000000000..007f5aaa35 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditRetryCodeVisionProvider.kt @@ -0,0 +1,13 @@ +package com.sourcegraph.cody.edit.lenses.providers + +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProviderMetadata + +class EditRetryCodeVisionProvider : EditCodeVisionProvider(EditRetryCodeVisionProvider) { + companion object : EditCodeVisionProviderMetadata() { + override val ordering: CodeVisionRelativeOrdering = + CodeVisionRelativeOrdering.CodeVisionRelativeOrderingLast + override val command: String = "cody.fixup.codelens.retry" + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditUndoCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditUndoCodeVisionProvider.kt new file mode 100644 index 0000000000..2e8b01bfd2 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditUndoCodeVisionProvider.kt @@ -0,0 +1,14 @@ +package com.sourcegraph.cody.edit.lenses.providers + +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.intellij.ui.JBColor +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProviderMetadata + +class EditUndoCodeVisionProvider : EditCodeVisionProvider(EditUndoCodeVisionProvider) { + companion object : EditCodeVisionProviderMetadata() { + override val ordering: CodeVisionRelativeOrdering = showAfter(EditAcceptCodeVisionProvider) + override val command: String = "cody.fixup.codelens.undo" + override val textColor: JBColor = JBColor.RED + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditWorkingCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditWorkingCodeVisionProvider.kt new file mode 100644 index 0000000000..b432ee260e --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/providers/EditWorkingCodeVisionProvider.kt @@ -0,0 +1,13 @@ +package com.sourcegraph.cody.edit.lenses.providers + +import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.EditCodeVisionProviderMetadata + +class EditWorkingCodeVisionProvider : EditCodeVisionProvider(EditWorkingCodeVisionProvider) { + companion object : EditCodeVisionProviderMetadata() { + override val ordering: CodeVisionRelativeOrdering = + CodeVisionRelativeOrdering.CodeVisionRelativeOrderingFirst + override val command: String = "cody.chat.focus" + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LabelHighlight.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LabelHighlight.kt deleted file mode 100644 index 8cdf27c489..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LabelHighlight.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.sourcegraph.cody.edit.widget - -import com.intellij.util.ui.UIUtil -import java.awt.Color -import java.awt.Graphics2D - -class LabelHighlight(private val color: Color) { - - /** - * Draws a highlighted background around the text centered in the widget. - * - * @param g the Graphics2D object to draw on. - * @param y the top of the highlight - * @param x the x-coordinate of the text's start position. - * @param textWidth the width of the text. - * @param textHeight the height of the text. - */ - fun drawHighlight(g: Graphics2D, x: Float, y: Float, textWidth: Int, textHeight: Int) { - // Draw shadow - g.color = UIUtil.shade(color, 0.5, 0.35) - g.fillRoundRect((x + 0).toInt(), (y + 0.5).toInt(), textWidth, textHeight, RADIUS, RADIUS) - - // Draw highlight - g.color = color - g.fillRoundRect(x.toInt(), y.toInt(), textWidth, textHeight, RADIUS, RADIUS) - } - - companion object { - const val RADIUS = 4 - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt deleted file mode 100644 index 984e6b5be4..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensAction.kt +++ /dev/null @@ -1,120 +0,0 @@ -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.intellij.util.ui.UIUtil -import com.sourcegraph.cody.edit.EditUtil -import com.sourcegraph.cody.edit.actions.lenses.EditAcceptAction -import com.sourcegraph.cody.edit.actions.lenses.EditUndoAction -import com.sourcegraph.cody.edit.actions.lenses.LensEditAction.Companion.TASK_ID_KEY -import java.awt.Font -import java.awt.FontMetrics -import java.awt.Graphics2D -import java.awt.geom.Rectangle2D -import java.util.concurrent.atomic.AtomicReference -import org.jetbrains.annotations.VisibleForTesting - -class LensAction( - val group: LensWidgetGroup, - val text: String, - @VisibleForTesting val actionId: String, - private val taskId: String? -) : LensWidget(group) { - - private val highlight = - LabelHighlight( - when (actionId) { - EditAcceptAction.ID -> acceptColor - EditUndoAction.ID -> undoColor - else -> actionColor - }) - - override fun calcWidthInPixels(fontMetrics: FontMetrics): Int { - return fontMetrics.stringWidth(text) + (2 * SIDE_MARGIN) - } - - override fun calcHeightInPixels(fontMetrics: FontMetrics): Int = fontMetrics.height - - override fun paint(g: Graphics2D, x: Float, y: Float) { - val originalFont = g.font - val originalColor = g.color - try { - g.background = EditUtil.getEnhancedThemeColor("Panel.background") - - val metrics = g.fontMetrics - val width = calcWidthInPixels(metrics) - 4 - val textHeight = metrics.height - - highlight.drawHighlight(g, x, y + 1, width, textHeight - 2) - - if (mouseInBounds) { - g.font = g.font.deriveFont(Font.BOLD).deriveFont(g.font.size * 0.85f) - g.color = UIUtil.shade(JBColor(0xFFFFFF, 0xFFFFFF), 1.0, 0.95) - } else { - g.font = g.font.deriveFont(Font.BOLD).deriveFont(g.font.size * 0.85f) - g.color = UIUtil.shade(JBColor(0xFFFFFF, 0xFFFFFF), 1.0, 0.9) - } - - g.drawString(text, x + SIDE_MARGIN, y + g.fontMetrics.ascent + 1) - - lastPaintedBounds = - Rectangle2D.Float(x, y - metrics.ascent, width.toFloat(), textHeight.toFloat()) - } finally { - g.font = originalFont - g.color = originalColor - } - } - - override fun onClick(e: EditorMouseEvent): Boolean { - triggerAction(e.editor) - return true - } - - fun triggerAction(editor: Editor) { - lastLensActionPerformed.set(actionId) - val action = ActionManager.getInstance().getAction(actionId) - if (action != null) { - val dataContext = createDataContext(editor, taskId) - val actionEvent = - AnActionEvent( - null, - dataContext, - "", - action.templatePresentation.clone(), - ActionManager.getInstance(), - 0) - action.actionPerformed(actionEvent) - } - } - - private fun createDataContext(editor: Editor, taskId: String?): DataContext { - return DataContext { dataId -> - when (dataId) { - PlatformDataKeys.CONTEXT_COMPONENT.name -> this - PlatformDataKeys.EDITOR.name -> editor - PlatformDataKeys.PROJECT.name -> editor.project - TASK_ID_KEY.name -> taskId - else -> null - } - } - } - - override fun toString(): String { - return "LensAction(text=$text)" - } - - companion object { - const val SIDE_MARGIN = 9 - - val actionColor = JBColor(0x4C4D54, 0x393B40) - private val acceptColor = JBColor(0x369650, 0x388119) - private val undoColor = JBColor(0xCC3645, 0x7B282C) - - private val lastLensActionPerformed: AtomicReference = AtomicReference(null) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt deleted file mode 100644 index cc69e37494..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensHotkey.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.sourcegraph.cody.edit.widget - -import com.intellij.ui.JBColor -import java.awt.Font -import java.awt.FontMetrics -import java.awt.Graphics2D - -class LensHotkey(group: LensWidgetGroup, text: String) : LensLabel(group, text) { - - private val hotkeyHighlightColor = JBColor(0xDDDDDD, 0x252629) - - private val highlight = LabelHighlight(hotkeyHighlightColor) - - override fun calcWidthInPixels(fontMetrics: FontMetrics): Int { - return fontMetrics.stringWidth(text) + 8 - } - - override fun paint(g: Graphics2D, x: Float, y: Float) { - // Resize font and get new metrics - val originalFont = g.font - val resizedFont = originalFont.deriveFont(Font.BOLD, originalFont.size * 0.8f) - g.font = resizedFont - val fontMetrics = g.fontMetrics - - // Calculate width and height with resized font - val width = fontMetrics.stringWidth(text) + 10 - val height = fontMetrics.height + 3 - - // Draw highlight - highlight.drawHighlight(g, x + 2, y + 1, width, height - 2) - - // Draw the text - g.color = JBColor(0x6F737A, 0x6F737A) - g.drawString(text, x + 7, y + fontMetrics.ascent + 1) - - // Restore original font - g.font = originalFont - } - - override fun toString(): String { - return "LensHotkey(text=$text)" - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensIcon.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensIcon.kt deleted file mode 100644 index d46695ce3a..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensIcon.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.sourcegraph.cody.edit.widget - -import java.awt.FontMetrics -import java.awt.Graphics2D -import java.awt.RenderingHints -import javax.swing.Icon - -class LensIcon(group: LensWidgetGroup, val icon: Icon) : LensWidget(group) { - - override fun calcWidthInPixels(fontMetrics: FontMetrics): Int { - val desiredHeight = (fontMetrics.height + fontMetrics.ascent) / 2.0f - val scaleFactor = desiredHeight / icon.iconHeight.toFloat() - return (icon.iconWidth * scaleFactor).toInt() - } - - override fun calcHeightInPixels(fontMetrics: FontMetrics): Int { - return ((fontMetrics.height + fontMetrics.ascent) / 2.0f).toInt() - } - - override fun paint(g: Graphics2D, x: Float, y: Float) { - val fontMetrics = g.fontMetrics - val textCenterLine = y + (fontMetrics.ascent + fontMetrics.descent) / 2.0f - val desiredHeight = (fontMetrics.height + fontMetrics.ascent) / 2.0f - val scaleFactor = desiredHeight / icon.iconHeight.toFloat() - val iconY = textCenterLine - desiredHeight / 2.0f - - // Set rendering hints for high quality resize - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) - - val originalTransform = g.transform - g.translate(x.toInt(), iconY.toInt()) - g.scale(scaleFactor.toDouble(), scaleFactor.toDouble()) - - // Paint the icon a bit down, presumably to account for the font baseline. - // This 1-"pixel" adjustment works surprisingly well for font sizes ranging - // from 7 to 24+, and I'm not sure why, but I tested across a wide range of sizes. - // If you take it out, the logo will be slightly too high and will drive you crazy. - icon.paintIcon(null, g, 0, 1) - - g.transform = originalTransform - } - - override fun toString(): String { - return "LensIcon(icon=$icon)" - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt deleted file mode 100644 index 741f9415e8..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensLabel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.sourcegraph.cody.edit.widget - -import com.intellij.openapi.editor.event.EditorMouseEvent -import com.intellij.ui.JBColor -import com.intellij.util.ui.UIUtil -import com.sourcegraph.cody.edit.LensesService.Companion.SEPARATOR -import java.awt.FontMetrics -import java.awt.Graphics2D - -open class LensLabel(group: LensWidgetGroup, val text: String) : LensWidget(group) { - - override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = fontMetrics.stringWidth(text) - - override fun calcHeightInPixels(fontMetrics: FontMetrics): Int = fontMetrics.height - - var hoverText: String? = null - - override fun onMouseEnter(e: EditorMouseEvent) { - mouseInBounds = true - showTooltip(hoverText ?: return, e.mouseEvent) - } - - override fun paint(g: Graphics2D, x: Float, y: Float) { - g.color = - if (text == SEPARATOR) { - UIUtil.shade(JBColor.foreground(), 1.0, 0.4) - } else { - baseTextColor - } - g.drawString(text, x, y + g.fontMetrics.ascent) - } - - override fun toString(): String { - return "LensLabel(text=$text)" - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt deleted file mode 100644 index d92ada1071..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensSpinner.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.sourcegraph.cody.edit.widget - -import java.awt.FontMetrics -import java.awt.Graphics2D -import javax.swing.Icon - -class LensSpinner(group: LensWidgetGroup, private val icon: Icon) : LensWidget(group) { - override fun paint(g: Graphics2D, x: Float, y: Float) { - icon.paintIcon(parentGroup.editor.contentComponent, g, x.toInt(), y.toInt()) - } - - override fun calcWidthInPixels(fontMetrics: FontMetrics): Int = icon.iconWidth - - override fun calcHeightInPixels(fontMetrics: FontMetrics): Int = icon.iconHeight -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt b/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt deleted file mode 100644 index 414e5a6585..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidget.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.sourcegraph.cody.edit.widget - -import com.intellij.openapi.Disposable -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.colors.ColorKey -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.colors.EditorColorsScheme -import com.intellij.openapi.editor.event.EditorMouseEvent -import com.intellij.openapi.ui.popup.Balloon -import com.intellij.ui.HintHint -import com.intellij.ui.JBColor -import com.intellij.ui.LightweightHint -import java.awt.Color -import java.awt.FontMetrics -import java.awt.Graphics2D -import java.awt.event.MouseEvent -import java.awt.geom.Rectangle2D -import javax.swing.BorderFactory -import javax.swing.JLabel -import javax.swing.UIManager - -abstract class LensWidget(val parentGroup: LensWidgetGroup) : Disposable { - protected val logger = Logger.getInstance(LensWidget::class.java) - - protected var mouseInBounds = false - - @Suppress("UseJBColor") protected val baseTextColor: Color = JBColor(0x393B40, 0xDFE1E5) - - private var showingTooltip = false - private var tooltip: LightweightHint? = null - - // Bounds of the last paint call, to check for clicks. - // Currently only set by LensActions, the only clickable widget type. - protected var lastPaintedBounds: Rectangle2D.Float? = null - - abstract fun calcWidthInPixels(fontMetrics: FontMetrics): Int - - abstract fun calcHeightInPixels(fontMetrics: FontMetrics): Int - - abstract fun paint(g: Graphics2D, x: Float, y: Float) - - /** Optional method for updating the widget state, useful for animations. */ - open fun update() {} - - /** Called only when widget is clicked. Coordinates are relative to the widget. */ - open fun onClick(e: EditorMouseEvent): Boolean { - return true - } - - open fun onMouseEnter(e: EditorMouseEvent) { - mouseInBounds = true - hideTooltip() - } - - open fun onMouseExit(e: EditorMouseEvent) { - mouseInBounds = false - hideTooltip() - } - - override fun dispose() { - hideTooltip() - } - - private val tooltipBackground = UIManager.getColor("ToolTip.background") - private val tooltipForeground = UIManager.getColor("ToolTip.foreground") - - protected fun showTooltip(text: String, e: MouseEvent) { - hideTooltip() - - val globalScheme: EditorColorsScheme = EditorColorsManager.getInstance().globalScheme - val tooltipLabel = - JLabel(text).apply { - foreground = - globalScheme.getColor( - ColorKey.createColorKey("TOOLTIP_FOREGROUND", tooltipForeground)) - ?: globalScheme.defaultForeground - background = - globalScheme.getColor( - ColorKey.createColorKey("TOOLTIP_BACKGROUND", tooltipBackground)) - ?: globalScheme.defaultBackground - isOpaque = true - font = parentGroup.widgetFont.get() - border = BorderFactory.createEmptyBorder(2, 8, 0, 8) - } - val hint = LightweightHint(tooltipLabel) - val p = parentGroup.widgetXY(this) - val hintHint = - HintHint(e.component, p) - .setPreferredPosition(Balloon.Position.above) - .setMayCenterPosition(false) - hint.show( - parentGroup.editor.contentComponent, - p.x, - p.y, - parentGroup.editor.contentComponent, - hintHint) - showingTooltip = true - tooltip = hint - } - - protected fun hideTooltip() { - if (showingTooltip) { - tooltip?.apply { - hide() - tooltip = null - } - showingTooltip = 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 deleted file mode 100644 index 544e4b0ff3..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/edit/widget/LensWidgetGroup.kt +++ /dev/null @@ -1,381 +0,0 @@ -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 -import com.intellij.openapi.editor.Inlay -import com.intellij.openapi.editor.ScrollType -import com.intellij.openapi.editor.colors.EditorColorsListener -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.colors.EditorFontType -import com.intellij.openapi.editor.event.EditorMouseEvent -import com.intellij.openapi.editor.event.EditorMouseListener -import com.intellij.openapi.editor.event.EditorMouseMotionListener -import com.intellij.openapi.editor.ex.util.EditorUtil -import com.intellij.openapi.editor.impl.EditorImpl -import com.intellij.openapi.editor.impl.FontInfo -import com.intellij.openapi.editor.markup.TextAttributes -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.TextRange -import com.intellij.ui.JBColor -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.UIUtil -import com.sourcegraph.cody.agent.protocol_extensions.toLogicalPosition -import com.sourcegraph.cody.agent.protocol_extensions.toOffsetRange -import com.sourcegraph.cody.agent.protocol_generated.Range -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.CompletableFuture -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import java.util.function.Supplier -import kotlin.math.roundToInt - -operator fun Point.component1() = this.x - -operator fun Point.component2() = this.y - -/** - * Manages a single code lens group. It should only be displayed once, and disposed after displaying - * it, before displaying another. - */ -class LensWidgetGroup(parentComponent: Editor) : EditorCustomElementRenderer, Disposable { - private val logger = Logger.getInstance(LensWidgetGroup::class.java) - val editor = parentComponent as EditorImpl - - val isDisposed = AtomicBoolean(false) - private val addedListeners = AtomicBoolean(false) - private val removedListeners = AtomicBoolean(false) - - val widgets = mutableListOf() - - private val mouseClickListener = - object : EditorMouseListener { - override fun mouseClicked(e: EditorMouseEvent) { - if (!listenersMuted) { - handleMouseClick(e) - } - } - } - private val mouseMotionListener = - object : EditorMouseMotionListener { - override fun mouseMoved(e: EditorMouseEvent) { - if (!listenersMuted) { - handleMouseMove(e) - } - } - } - - private var listenersMuted = false - - val widgetFont = AtomicReference(UIUtil.getLabelFont()) - - // Compute inlay height based on the widget font, not the editor font. - private val inlayHeight = AtomicReference(computeInlayHeight()) - - private var widgetFontMetrics: FontMetrics? = null - - private var lastHoveredWidget: LensWidget? = null // Used for mouse rollover highlighting. - - var inlay: Inlay? = null - - private var lastComputedIndent = RECOMPUTE - - init { - editor.addEditorMouseListener(mouseClickListener) - editor.addEditorMouseMotionListener(mouseMotionListener) - addedListeners.set(true) - - // Listen for color theme changes. - ApplicationManager.getApplication() - .messageBus - .connect(this) - .subscribe( - EditorColorsManager.TOPIC, - EditorColorsListener { - updateFonts() - update() - }) - } - - private fun updateFonts() { - val font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) - widgetFont.set(Font(font.name, font.style, font.size)) - widgetFontMetrics = null // force recalculation - } - - @RequiresEdt - fun show(range: Range, shouldScrollToLens: Boolean) { - val (startOffset, _) = range.toOffsetRange(editor.document) - if (isDisposed.get()) { - throw IllegalStateException("Request to show disposed inlay: $this") - } - inlay = editor.inlayModel.addBlockElement(startOffset, false, true, 0, this) - Disposer.register(this, inlay!!) - - if (shouldScrollToLens) { - val logicalPosition = range.start.toLogicalPosition(editor.document) - editor.scrollingModel.scrollTo(logicalPosition, ScrollType.CENTER) - } - } - - // Propagate repaint requests from widgets to the inlay. - fun update() { - inlayHeight.set(computeInlayHeight()) - inlay?.apply { - update() - repaint() - } - } - - private fun computeInlayHeight(): Int { - val font = - Font( - editor.colorsScheme.fontPreferences.fontFamily, - widgetFont.get().style, - widgetFont.get().size) - val fontMetrics = - FontInfo.getFontMetrics(font, FontInfo.getFontRenderContext(editor.contentComponent)) - - val totalHeight = fontMetrics.ascent + fontMetrics.descent - - return (totalHeight * INLAY_HEIGHT_SCALE_FACTOR).roundToInt() - } - - override fun calcWidthInPixels(inlay: Inlay<*>): Int { - return editor.component.width - } - - override fun calcHeightInPixels(inlay: Inlay<*>): Int { - return inlayHeight.get() - } - - private fun widgetGroupXY(): Point { - return editor.offsetToXY(inlay?.offset ?: return Point(0, 0)) - } - - fun widgetXY(widget: LensWidget): Point { - val ourXY = widgetGroupXY() - val fontMetrics = widgetFontMetrics ?: editor.getFontMetrics(Font.PLAIN) - var sum = leftMargin() - for (w in widgets) { - if (w == widget) break - sum += w.calcWidthInPixels(fontMetrics) - } - return Point(ourXY.x + sum, ourXY.y) - } - - override fun paint( - inlay: Inlay<*>, - g: Graphics2D, - targetRegion: Rectangle2D, - textAttributes: TextAttributes - ) { - g.font = widgetFont.get() - if (widgetFontMetrics == null) { // Cache for hit box detection later. - widgetFontMetrics = g.fontMetrics - } - - val inlayHeight = calcHeightInPixels(inlay) - - // Draw the inlay background across the width of the Editor. - g.color = JBColor(0xECEDF2, 0x26282D) - g.fillRect( - targetRegion.x.roundToInt(), - targetRegion.y.roundToInt(), - calcWidthInPixels(inlay), - inlayHeight) - - val fontMetrics = g.fontMetrics - val fontHeight = fontMetrics.ascent + fontMetrics.descent - val verticalPadding = (inlayHeight - fontHeight) / 2 - - val top = targetRegion.y + verticalPadding - val left = targetRegion.x + leftMargin() - - // Draw all the widgets left to right, keeping track of their x-position. - widgets.fold(left) { acc, widget -> - try { - widget.paint(g, acc.toFloat(), top.toFloat()) - acc + widget.calcWidthInPixels(g.fontMetrics) - } finally { - g.font = widgetFont.get() - } - } - } - - private fun findWidgetAt(x: Int, y: Int): LensWidget? { - var currentX = leftMargin() - val fontMetrics = widgetFontMetrics ?: return null - if (inlay?.bounds?.contains(x, y) == false) return null - - // Walk widgets left to right checking their hit boxes. - for (widget in widgets) { - val widgetWidth = widget.calcWidthInPixels(fontMetrics) - val rightEdgeX = currentX + widgetWidth - if (x >= currentX && x <= rightEdgeX) { // In widget's bounds? - return widget - } - currentX = rightEdgeX - } - return null - } - - fun addWidget(widget: LensWidget) { - widgets.add(widget) - Disposer.register(this, widget) - } - - // Computes the X coordinate in the Editor where the first widget is drawn. - private fun leftMargin(): Int { - if (lastComputedIndent != RECOMPUTE) { - return lastComputedIndent - } - - try { - val document = editor.document - val inlayOffset = inlay?.offset - if (inlayOffset == null) { - lastComputedIndent = DEFAULT_MARGIN - return DEFAULT_MARGIN - } - - val lineCount = document.lineCount - val inlayLineNumber = document.getLineNumber(inlayOffset) - - // Find next non-blank line. - for (lineNumber in inlayLineNumber until lineCount) { - val lineStartOffset = document.getLineStartOffset(lineNumber) - val lineEndOffset = document.getLineEndOffset(lineNumber) - val lineText = document.getText(TextRange(lineStartOffset, lineEndOffset)) - - // Compute the pixel width of the indentation. - if (lineText.isNotBlank()) { - val tabSize = EditorUtil.getTabSize(editor) - val spaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, editor) - val indentationLevel = - lineText - .takeWhile { it.isWhitespace() } - .sumOf { if (it == '\t') tabSize * spaceWidth else spaceWidth } - - lastComputedIndent = indentationLevel - return indentationLevel - } - } - } catch (x: Exception) { - logger.warn("Error computing code lens left margin", x) - } - lastComputedIndent = DEFAULT_MARGIN - return DEFAULT_MARGIN - } - - fun reset() { - lastComputedIndent = RECOMPUTE - } - - // Dispatch mouse click events to the appropriate widget. - private fun handleMouseClick(e: EditorMouseEvent) { - val (x, y) = e.mouseEvent.point - if (findWidgetAt(x, y)?.onClick(e) == true) { - e.consume() - } - } - - private fun handleMouseMove(e: EditorMouseEvent) { - val (x, y) = e.mouseEvent.point - val widget = findWidgetAt(x, y) - val lastWidget = lastHoveredWidget - - if (widget is LensAction) { - e.editor.contentComponent.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) - } else { - e.editor.contentComponent.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) - } - - // Check if the mouse has moved from one widget to another or from/to outside. - if (widget != lastWidget) { - lastWidget?.onMouseExit(e) - lastHoveredWidget = widget // null if now outside - widget?.onMouseEnter(e) - inlay?.update() // force repaint - } - } - - /** Immediately 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 - 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) { - Disposer.dispose(this) - } - inlay = null - } - } - - private fun onEventThread(handler: Supplier): CompletableFuture { - val result = CompletableFuture() - val executeAndComplete: () -> Unit = { - try { - result.complete(handler.get()) - } catch (e: Exception) { - result.completeExceptionally(e) - } - } - if (ApplicationManager.getApplication().isDispatchThread) { - executeAndComplete() - } else { - runInEdt { executeAndComplete() } - } - return result - } - - override fun toString(): String { - val render = widgets.joinToString(separator = ",") { it.toString() } - return "LensWidgetGroup: {$render}" - } - - companion object { - private const val DEFAULT_MARGIN = 20 - - // The height of the inlay is always scaled to the font height, - // with room for the buttons and some top/bottom padding. This setting - // 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. - private const val RECOMPUTE = -1 - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt b/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt deleted file mode 100644 index ca778a84c9..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/FrameMover.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runInEdt -import com.intellij.util.ui.UIUtil -import com.sourcegraph.cody.edit.EditUtil -import com.sourcegraph.cody.edit.widget.component1 -import com.sourcegraph.cody.edit.widget.component2 -import java.awt.Component -import java.awt.Container -import java.awt.Cursor -import java.awt.Point -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.awt.event.MouseMotionAdapter -import javax.swing.JComponent -import javax.swing.JFrame -import javax.swing.SwingUtilities - -// Provides undecorated JFrames with the ability to be moved and resized. -class FrameMover(private val frame: JFrame, private val titleBar: JComponent) : Disposable { - - private var resizeDirection = ResizeDirection.NONE - private var lastMouseX = 0 - private var lastMouseY = 0 - - // Debounce to mitigate jitter while dragging. - private var lastUpdateTime = System.currentTimeMillis() - - private val frameMouseListener = - object : MouseAdapter() { - override fun mousePressed(e: MouseEvent) = handleMousePressed(e) - - override fun mouseReleased(e: MouseEvent) = handleMouseReleased() - - override fun mouseEntered(e: MouseEvent) = handleMouseEntered() - - override fun mouseExited(e: MouseEvent) = handleMouseExited() - - override fun mouseClicked(e: MouseEvent) = handleMouseClicked() - } - - private val frameMouseMotionListener = - object : MouseMotionAdapter() { - override fun mouseMoved(e: MouseEvent) = handleMouseMoved(e) - - override fun mouseDragged(e: MouseEvent) = handleMouseDragged(e) - } - - init { - frame.addMouseListener(frameMouseListener) - frame.addMouseMotionListener(frameMouseMotionListener) - - addForwardingListeners(titleBar) - } - - private fun addForwardingListeners(component: Component) { - if (component is JComponent) { - component.addMouseListener(ForwardingAdapter(component)) - component.addMouseMotionListener(ForwardingAdapter(component)) - } - if (component is Container) { - component.components.forEach { addForwardingListeners(it) } - } - } - - private fun updateCursor() { - frame.cursor = - when (resizeDirection) { - ResizeDirection.NORTH_WEST -> Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR) - ResizeDirection.NORTH -> Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR) - ResizeDirection.NORTH_EAST -> Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR) - ResizeDirection.WEST -> Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR) - ResizeDirection.EAST -> Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR) - ResizeDirection.SOUTH_WEST -> Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR) - ResizeDirection.SOUTH -> Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR) - ResizeDirection.SOUTH_EAST -> Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR) - else -> Cursor.getDefaultCursor() - } - } - - // See if the point, relative to our frame coordinates, is in a resize zone. - private fun getResizeDirection(point: Point): ResizeDirection { - val border = RESIZE_BORDER - val (px, py) = point - val w = frame.width - val h = frame.height - return when { - px < border && py < border -> ResizeDirection.NORTH_WEST - px < border && py >= h - border -> ResizeDirection.SOUTH_WEST - px < border -> ResizeDirection.WEST - px >= w - border && py < border -> ResizeDirection.NORTH_EAST - px >= w - border && py >= h - border -> ResizeDirection.SOUTH_EAST - px >= w - border -> ResizeDirection.EAST - py < border -> ResizeDirection.NORTH - py >= h - border -> ResizeDirection.SOUTH - else -> ResizeDirection.NONE - }.also { resizeDirection = it } - } - - // These methods all take an event in the Frame's coordinate space. - private fun handleMousePressed(e: MouseEvent) { - resizeDirection = getResizeDirection(e.point) - lastMouseX = e.xOnScreen - lastMouseY = e.yOnScreen - updateCursor() - } - - private fun handleMouseReleased() { - resizeDirection = ResizeDirection.NONE - frame.cursor = Cursor.getDefaultCursor() - } - - private fun handleMouseClicked() {} - - private fun handleMouseEntered() = updateCursor() - - private fun handleMouseExited() = updateCursor() - - private fun handleMouseMoved(e: MouseEvent) { - resizeDirection = getResizeDirection(e.point) - updateCursor() - } - - private fun handleMouseDragged(e: MouseEvent) { - if (resizeDirection != ResizeDirection.NONE) { - resizeDialog(e) - updateCursor() - } else { - moveDialog(e) - } - } - - private fun resizeDialog(e: MouseEvent) { - val currentTime = System.currentTimeMillis() - if (currentTime - lastUpdateTime <= 16) return - - val newX = e.xOnScreen - val newY = e.yOnScreen - val deltaX = newX - lastMouseX - val deltaY = newY - lastMouseY - - var newWidth = frame.width - var newHeight = frame.height - val minimumSize = frame.minimumSize - val x = frame.location.x - val y = frame.location.y - - when (resizeDirection) { - ResizeDirection.EAST, - ResizeDirection.NORTH_EAST, - ResizeDirection.SOUTH_EAST -> { - newWidth = minimumSize.width.coerceAtLeast(frame.width + deltaX) - } - ResizeDirection.WEST, - ResizeDirection.NORTH_WEST, - ResizeDirection.SOUTH_WEST -> { - newWidth = minimumSize.width.coerceAtLeast(frame.width - deltaX) - frame.setLocation(x + deltaX, y) - } - else -> {} - } - when (resizeDirection) { - ResizeDirection.SOUTH, - ResizeDirection.SOUTH_EAST, - ResizeDirection.SOUTH_WEST -> { - newHeight = minimumSize.height.coerceAtLeast(frame.height + deltaY) - } - ResizeDirection.NORTH, - ResizeDirection.NORTH_EAST, - ResizeDirection.NORTH_WEST -> { - newHeight = minimumSize.height.coerceAtLeast(frame.height - deltaY) - frame.setLocation(x, y + deltaY) - } - else -> {} - } - SwingUtilities.invokeLater { - frame.setSize(newWidth, newHeight) - frame.revalidate() - frame.repaint() - } - lastMouseX = newX - lastMouseY = newY - lastUpdateTime = currentTime - } - - private fun moveDialog(e: MouseEvent) { - val currentTime = System.currentTimeMillis() - if (currentTime - lastUpdateTime > 16) { // about 60 fps - val x: Int = e.xOnScreen - val y: Int = e.yOnScreen - runInEdt { - frame.rootPane?.let { rootPane -> - UIUtil.getLocationOnScreen(rootPane)?.let { loc -> - frame.setLocation(loc.x + x - lastMouseX, loc.y + y - lastMouseY) - lastMouseX = x - lastMouseY = y - } - } - } - lastUpdateTime = currentTime - } - } - - override fun dispose() { - EditUtil.removeAllListeners(titleBar) - frame.removeMouseListener(frameMouseListener) - frame.removeMouseMotionListener(frameMouseMotionListener) - } - - private enum class ResizeDirection { - NORTH_WEST, - NORTH, - NORTH_EAST, - WEST, - EAST, - SOUTH_WEST, - SOUTH, - SOUTH_EAST, - NONE - } - - private inner class ForwardingAdapter(val component: Component) : MouseAdapter() { - override fun mousePressed(e: MouseEvent) = handleMousePressed(translate(e)) - - override fun mouseMoved(e: MouseEvent) = handleMouseMoved(translate(e)) - - override fun mouseDragged(e: MouseEvent) = handleMouseDragged(translate(e)) - - override fun mouseEntered(e: MouseEvent) = handleMouseEntered() - - override fun mouseExited(e: MouseEvent) = handleMouseExited() - - override fun mouseClicked(e: MouseEvent) = handleMouseClicked() - - override fun mouseReleased(e: MouseEvent) = handleMouseReleased() - - private fun translate(e: MouseEvent) = SwingUtilities.convertMouseEvent(component, e, frame) - } - - companion object { - private const val RESIZE_BORDER = 6 - } -} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 68e98a8d1b..17a72989ab 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -104,6 +104,26 @@ (or a subtype.) For now, VSCode view "when" conditions need to be manually ported to ToolWindow.isAvailable updates. --> + + + + + + + + @@ -242,7 +262,7 @@ @@ -251,7 +271,7 @@ @@ -260,7 +280,7 @@ @@ -269,7 +289,7 @@ @@ -279,7 +299,7 @@ From 91f59ab7fe04e4b5224f8ddd02e8411792645499 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Fri, 20 Sep 2024 09:22:14 +0200 Subject: [PATCH 2/8] Add settings provider --- .../lenses/EditCodeVisionGroupSettingProvider.kt | 12 ++++++++++++ src/main/resources/META-INF/plugin.xml | 13 +++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionGroupSettingProvider.kt diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionGroupSettingProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionGroupSettingProvider.kt new file mode 100644 index 0000000000..3c44a19aaa --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionGroupSettingProvider.kt @@ -0,0 +1,12 @@ +package com.sourcegraph.cody.edit.lenses + +import com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider + +class EditCodeVisionGroupSettingProvider : CodeVisionGroupSettingProvider { + override val groupId: String = "EditCodeVisionProvider" + + override val groupName: String = "Cody Edit Lenses" + + override val description: String = + "Lenses used by Cody for displaying control actions for the given edit" +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 17a72989ab..e7c2d915bc 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -106,24 +106,25 @@ --> + From 7617f0c4f47f963774d01d15b8ef32d4d688c299 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Fri, 20 Sep 2024 09:22:46 +0200 Subject: [PATCH 3/8] Add tests --- gradle.properties | 2 +- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 104 +++------- .../cody/util/CodyIntegrationTextFixture.kt | 85 +++----- .../recording.har.yaml | 188 ++++++++++-------- .../com/sourcegraph/cody/agent/CodyAgent.kt | 1 + .../cody/edit/lenses/LensesService.kt | 48 +++-- 6 files changed, 201 insertions(+), 227 deletions(-) diff --git a/gradle.properties b/gradle.properties index c5b387a228..3f7182e2df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ kotlin.daemon.jvmargs=-Xmx2g -Xms500m nodeBinaries.commit=8755ae4c05fd476cd23f2972049111ba436c86d4 nodeBinaries.version=v20.12.2 cody.autocomplete.enableFormatting=true -cody.commit=daa1693620fdf2784ecd190dd12bb07169950a90 +cody.commit=5bd253c16261680490d2afb14398ff568334ffbd diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 271b021b6d..c42559b933 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -1,21 +1,17 @@ package com.sourcegraph.cody.edit import com.sourcegraph.cody.edit.actions.DocumentCodeAction -import com.sourcegraph.cody.edit.lenses.LensesService import com.sourcegraph.cody.edit.lenses.actions.EditAcceptAction import com.sourcegraph.cody.edit.lenses.actions.EditCancelAction import com.sourcegraph.cody.edit.lenses.actions.EditUndoAction -import com.sourcegraph.cody.edit.lenses.widget.LensActionWidget -import com.sourcegraph.cody.edit.lenses.widget.LensHotkeyWidget -import com.sourcegraph.cody.edit.lenses.widget.LensIconWidget -import com.sourcegraph.cody.edit.lenses.widget.LensLabelWidget -import com.sourcegraph.cody.edit.lenses.widget.LensSpinnerWidget -import com.sourcegraph.cody.edit.lenses.widget.LensWidgetGroup +import com.sourcegraph.cody.edit.lenses.providers.EditAcceptCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditCancelCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditUndoCodeVisionProvider +import com.sourcegraph.cody.edit.lenses.providers.EditWorkingCodeVisionProvider import com.sourcegraph.cody.util.CodyIntegrationTextFixture import com.sourcegraph.cody.util.CustomJunitClassRunner import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.startsWith -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -23,97 +19,63 @@ import org.junit.runner.RunWith class DocumentCodeTest : CodyIntegrationTextFixture() { @Test - @Ignore fun testGetsWorkingGroupLens() { - val codeLensGroup = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID) + val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID) - val inlayModel = myFixture.editor.inlayModel - val blockElements = inlayModel.getBlockElementsInRange(0, myFixture.editor.document.textLength) - val lensesGroups = blockElements.mapNotNull { it.renderer as? LensWidgetGroup } - - assertEquals("There should be exactly one lenses group", 1, lensesGroups.size) - - assertTrue("codeLensGroup cannot be null", codeLensGroup != null) + assertEquals("There are 2 lenses expected, working lens and cancel lens", 2, codeLenses.size) // Lens group should match the expected structure. - val theWidgets = codeLensGroup!!.widgets - - assertEquals("Lens group should have 9 widgets", 9, theWidgets.size) - assertTrue("Zeroth lens group should be an icon", theWidgets[0] is LensIconWidget) - assertTrue( - "First lens group is space separator label", (theWidgets[1] as LensLabelWidget).text == " ") - assertTrue("Second lens group is a spinner", theWidgets[2] is LensSpinnerWidget) - assertTrue( - "Third lens group is space separator label", (theWidgets[3] as LensLabelWidget).text == " ") - assertTrue( - "Fourth lens group is a description label", - (theWidgets[4] as LensActionWidget).text == " Cody is working...") - assertTrue( - "Fifth lens group is separator label", - (theWidgets[5] as LensLabelWidget).text == LensesService.SEPARATOR) - assertTrue("Sixth lens group should be an action", theWidgets[6] is LensActionWidget) - assertTrue( - "Seventh lens group should be a label with a hotkey", theWidgets[7] is LensHotkeyWidget) + assertEquals( + "First lens should be working lens", + codeLenses[0].command?.command, + EditWorkingCodeVisionProvider.command) + assertEquals( + "Second lens should be cancel lens", + codeLenses[1].command?.command, + EditCancelCodeVisionProvider.command) - runLensAction(codeLensGroup, EditCancelAction.ID) - assertNoInlayShown() + runAndWaitForCleanState(EditCancelAction.ID) } @Test fun testShowsAcceptLens() { - val codeLensGroup = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID) - assertInlayIsShown() + val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID) + assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty()) - // Lens group should match the expected structure. - val inlayModel = myFixture.editor.inlayModel - val blockElements = inlayModel.getBlockElementsInRange(0, myFixture.editor.document.textLength) - val lensesGroups = blockElements.mapNotNull { it.renderer as? LensWidgetGroup } - val lenses = lensesGroups.firstOrNull() - - assertNotNull("Lens group should be displayed", lenses) - - val widgets = lenses!!.widgets - // There are 13 widgets as of the time of writing, but the UX could change, so check robustly. - assertTrue("Lens group should have at least 4 widgets", widgets.size >= 4) - assertNotNull( - "Lens group should contain Accept action", - widgets.find { widget -> - widget is LensActionWidget && widget.actionId == EditAcceptAction.ID - }) - assertNotNull( - "Lens group should contain Show Undo action", - widgets.find { widget -> - widget is LensActionWidget && widget.actionId == EditUndoAction.ID - }) + assertEquals("Lens group should have 4 lenses", 2, codeLenses.size) + assertEquals( + "First lens should be accept lens", + codeLenses[0].command?.command, + EditAcceptCodeVisionProvider.command) + assertEquals( + "Second lens should be undo lens", + codeLenses[1].command?.command, + EditUndoCodeVisionProvider.command) // Make sure a doc comment was inserted. assertTrue(hasJavadocComment(myFixture.editor.document.text)) - runLensAction(codeLensGroup!!, EditUndoAction.ID) - assertNoInlayShown() + runAndWaitForCleanState(EditUndoAction.ID) } @Test fun testAccept() { assertNoInlayShown() - val acceptLens = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID) - assertTrue("Accept lens should be displayed", acceptLens != null) - assertInlayIsShown() + val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID) + assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty()) - runLensAction(acceptLens!!, EditAcceptAction.ID) - assertNoInlayShown() + runAndWaitForCleanState(EditAcceptAction.ID) assertThat(myFixture.editor.document.text, startsWith("/**")) } @Test fun testUndo() { val originalDocument = myFixture.editor.document.text - val undoLens = runAndWaitForLenses(DocumentCodeAction.ID, EditUndoAction.ID) - assertTrue("Undo lens should be displayed", undoLens != null) + val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditUndoAction.ID) + assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty()) assertNotSame( "Expected document to be changed", originalDocument, myFixture.editor.document.text) - assertInlayIsShown() - runLensAction(undoLens!!, EditUndoAction.ID) + runAndWaitForCleanState(EditUndoAction.ID) assertEquals( "Expected document changes to be reverted", originalDocument, diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt index 31d44ca85d..4c605324e2 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -24,8 +25,6 @@ import com.sourcegraph.cody.config.CodyPersistentAccountsHost import com.sourcegraph.cody.config.SourcegraphServerPath import com.sourcegraph.cody.edit.lenses.LensListener import com.sourcegraph.cody.edit.lenses.LensesService -import com.sourcegraph.cody.edit.lenses.widget.LensActionWidget -import com.sourcegraph.cody.edit.lenses.widget.LensWidgetGroup import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern @@ -33,9 +32,7 @@ import junit.framework.TestCase open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener { private val logger = Logger.getInstance(CodyIntegrationTextFixture::class.java) - private val lensSubscribers = - mutableListOf< - Pair<(List) -> Boolean, CompletableFuture>>() + private val lensSubscribers = mutableListOf<(List) -> Boolean>() override fun setUp() { super.setUp() @@ -205,73 +202,43 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener { } } - protected fun assertInlayIsShown() { - runInEdtAndWait { - PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - assertTrue( - "Lens group inlay should be displayed", myFixture.editor.inlayModel.hasBlockElements()) - } + override fun onLensesUpdate(vf: VirtualFile, codeLenses: List) { + synchronized(lensSubscribers) { lensSubscribers.removeAll { it(codeLenses) } } } - override fun onLensesUpdate( - lensWidgetGroup: LensWidgetGroup?, - codeLenses: List - ) { - synchronized(lensSubscribers) { - lensSubscribers.removeAll { (checkFunc, future) -> - if (codeLenses.find { it.command?.command == "cody.fixup.codelens.error" } != null) { - future.completeExceptionally(IllegalStateException("Error group shown")) - return@removeAll true - } - - val hasLensAppeared = checkFunc(codeLenses) - if (hasLensAppeared) future.complete(lensWidgetGroup) - hasLensAppeared - } - } + fun runAndWaitForCleanState(actionIdToRun: String) { + runAndWaitForLenses(actionIdToRun) } - fun runAndWaitForLenses(actionId: String, actionLensId: String): LensWidgetGroup? { - val future = CompletableFuture() - val check = { codeLens: List -> - codeLens.any { it.command?.command == actionLensId } - } - lensSubscribers.add(check to future) - - triggerAction(actionId) + fun runAndWaitForLenses( + actionIdToRun: String, + vararg expectedLenses: String + ): List { + val future = CompletableFuture>() + lensSubscribers.add { codeLenses -> + val error = codeLenses.find { it.command?.command == "cody.fixup.codelens.error" } + if (error != null) { + future.completeExceptionally( + IllegalStateException("Error group shown: ${error.command?.title}")) + return@add true + } - try { - return future.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - } catch (e: Exception) { - val stackTrace = e.stackTrace.joinToString("\n") { it.toString() } - assertTrue( - "Error while awaiting condition after action $actionId: ${e.localizedMessage}\n$stackTrace", - false) - throw e + if ((expectedLenses.isEmpty() && codeLenses.isEmpty()) || + expectedLenses.all { expected -> codeLenses.any { it.command?.command == expected } }) { + future.complete(codeLenses) + return@add true + } + return@add false } - } - - fun runLensAction(lensWidgetGroup: LensWidgetGroup, actionLensId: String): LensWidgetGroup? { - val future = CompletableFuture() - val check = { codeLens: List -> codeLens.isEmpty() } - lensSubscribers.add(check to future) - runInEdtAndWait { - val action: LensActionWidget? = - lensWidgetGroup.widgets.filterIsInstance().find { - it.actionId == actionLensId - } - PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - assertTrue("Lens action $actionLensId should be available", action != null) - action?.triggerAction(myFixture.editor) - } + triggerAction(actionIdToRun) try { return future.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) } catch (e: Exception) { val stackTrace = e.stackTrace.joinToString("\n") { it.toString() } assertTrue( - "Error while awaiting condition after lens action $actionLensId: ${e.localizedMessage}\n$stackTrace", + "Error while awaiting condition after action $actionIdToRun: ${e.localizedMessage}\n$stackTrace", false) throw e } 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 ecf79ac81a..bab3c35e3f 100644 --- a/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml +++ b/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml @@ -5,7 +5,7 @@ log: name: Polly.JS version: 6.0.6 entries: - - _id: 8ffa79cf5668b64b3fd5ad5674e1d846 + - _id: 4639cede1d1b8af7aafd2d4f4e5060d6 _order: 0 cache: {} request: @@ -15,7 +15,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -30,24 +30,25 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 297 + headersSize: 370 httpVersion: HTTP/1.1 method: GET queryString: [] url: https://sourcegraph.com/.api/client-config response: - bodySize: 191 + bodySize: 227 content: encoding: base64 mimeType: text/plain; charset=utf-8 - size: 191 - text: "[\"H4sIAAAA\",\"AAAAA2zMsQoCMRCE4T5PsVztE9hJsLjOznrPrBjI7koyQeW4d7dRBEn9\ - z3xrICKaLp5eR+OlSJr2hNpl9wk3xjBwh0fXexHI+NkbXKOrsqU2NoCal47s9utXLu0\ - 7aMoV0Q3yxDlb8sfQUU9S2uE0/ylhC28AAAD//wMAK/Ow9eAAAAA=\"]" + size: 227 + text: "[\"H4sIAAAA\",\"AAAAA2yOsQrCMBRF935F6Ozk2K0Eh26Fgs6v5omBvLySd4OK+O8uFZfM\ + 59zDfXfOOddfNbxOmdbEoR8cSuXDDu6EJqAK9SpbYnB7WQ0qXkUoB2s3gBLXiqj5z2+\ + U7CeYUIHXDH7iEnPQR7MjGjjZOE/tSiKwYanbpgUc9tdRsy0oTDLO05mLRc394I7dp/\ + sCAAD//wMARx9M/RUBAAA=\"]" cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:14 GMT - name: content-type value: text/plain; charset=utf-8 - name: transfer-encoding @@ -78,7 +79,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:28.618Z + startedDateTime: 2024-09-20T10:35:13.980Z time: 0 timings: blocked: -1 @@ -88,7 +89,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 0d17b1a261e95ae22c4a5fe820c435a4 + - _id: b3144191d81f7224631c79665cc65164 _order: 0 cache: {} request: @@ -101,11 +102,11 @@ log: value: gzip;q=0 - name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - name: user-agent value: JetBrains / 6.0-localbuild - name: traceparent - value: 00-7782df09d434800b6d247b9d627f73b6-2bf4d058fde5682f-01 + value: 00-3242a758b262e2c7e69b4ccdc616bf25-ffe701046f988c12-01 - name: connection value: keep-alive - name: host @@ -206,14 +207,14 @@ log: value: 6.0-localbuild url: https://sourcegraph.com/.api/completions/stream?api-version=2&client-name=jetbrains&client-version=6.0-localbuild response: - bodySize: 535 + bodySize: 672 content: mimeType: text/event-stream - size: 535 + size: 672 text: >+ event: completion - data: {"deltaText":"/**\n * Imports the ArrayList and List classes from the java.util package.\n */","stopReason":"stop_sequence"} + data: {"deltaText":"/**\n * Imports the necessary Java utilities, including the ArrayList data structure.\n */","stopReason":"stop_sequence"} event: done @@ -223,13 +224,15 @@ log: cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:31 GMT + value: Fri, 20 Sep 2024 10:35:17 GMT - name: content-type value: text/event-stream - name: transfer-encoding value: chunked - name: connection value: keep-alive + - name: retry-after + value: "598" - name: access-control-allow-credentials value: "true" - name: access-control-allow-origin @@ -247,12 +250,12 @@ log: value: 1; mode=block - name: strict-transport-security value: max-age=31536000; includeSubDomains; preload - headersSize: 1299 + headersSize: 1406 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:30.126Z + startedDateTime: 2024-09-20T10:35:16.531Z time: 0 timings: blocked: -1 @@ -262,7 +265,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 460fa85fef77ffa15bb82fa3a88049a3 + - _id: e91b84ea768b5eed28380699acd05fc6 _order: 0 cache: {} request: @@ -272,7 +275,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -290,7 +293,7 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 345 + headersSize: 418 httpVersion: HTTP/1.1 method: POST postData: @@ -339,7 +342,7 @@ log: cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:13 GMT - name: content-type value: application/json - name: transfer-encoding @@ -365,12 +368,12 @@ log: value: max-age=31536000; includeSubDomains; preload - name: content-encoding value: gzip - headersSize: 1333 + headersSize: 1473 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:27.746Z + startedDateTime: 2024-09-20T10:35:13.014Z time: 0 timings: blocked: -1 @@ -380,7 +383,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 7edf90ea650cb304fe26f9d57bd79477 + - _id: d0a9ba8f1dcebcf03586956633e53ce8 _order: 0 cache: {} request: @@ -390,7 +393,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -408,7 +411,7 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 345 + headersSize: 418 httpVersion: HTTP/1.1 method: POST postData: @@ -430,17 +433,22 @@ log: value: null url: https://sourcegraph.com/.api/graphql?CurrentSiteCodyLlmConfiguration response: - bodySize: 135 + bodySize: 132 content: encoding: base64 mimeType: application/json - size: 135 - text: "[\"H4sIAAAAAAAAA6pWSkks\",\"SVSyqlYqzixJBdHJ+SmVPj6+zvl5aZnppUWJJZn5eWD5\ - 3MSiEuf8vJLUipLwzLyU/HIlK6XUvMSknNQUpdra2loAAAAA//8DAOgINKVLAAAA\"]" + size: 132 + text: "[\"H4sIAAAAAAAAA6pWSkksSVSyqlYqzixJBdHJ+SmVPj6+zvl5aZnppUWJJZn5eWD53MSiE\ + uf8vJLUipLwzLyU/HIlK6XUvMSknNQUpdra2loAAAAA//8DAOgINKVLAAAA\"]" + textDecoded: + data: + site: + codyLLMConfiguration: + smartContextWindow: enabled cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:13 GMT - name: content-type value: application/json - name: transfer-encoding @@ -466,12 +474,12 @@ log: value: max-age=31536000; includeSubDomains; preload - name: content-encoding value: gzip - headersSize: 1333 + headersSize: 1473 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:27.780Z + startedDateTime: 2024-09-20T10:35:13.047Z time: 0 timings: blocked: -1 @@ -481,7 +489,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: d890a82b3cc2fe4d514f289e8fe6d158 + - _id: 94049b00a1d0bb35daab2f7a3b6eb687 _order: 0 cache: {} request: @@ -491,7 +499,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -509,7 +517,7 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 340 + headersSize: 413 httpVersion: HTTP/1.1 method: POST postData: @@ -546,7 +554,7 @@ log: cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:13 GMT - name: content-type value: application/json - name: transfer-encoding @@ -572,12 +580,12 @@ log: value: max-age=31536000; includeSubDomains; preload - name: content-encoding value: gzip - headersSize: 1333 + headersSize: 1473 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:27.763Z + startedDateTime: 2024-09-20T10:35:13.031Z time: 0 timings: blocked: -1 @@ -587,7 +595,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 3fec40886d87367e761715c3159e6590 + - _id: 48b3a6bf0b107e0d034aa592bc6e86b2 _order: 0 cache: {} request: @@ -597,7 +605,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -615,7 +623,7 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 325 + headersSize: 398 httpVersion: HTTP/1.1 method: POST postData: @@ -648,21 +656,33 @@ log: value: null url: https://sourcegraph.com/.api/graphql?CurrentUser response: - bodySize: 379 + bodySize: 376 content: encoding: base64 mimeType: application/json - size: 379 - text: "[\"H4sIAAAAAAAAA2RPy04=\",\"wkAU/Ze7bmkNUdpJSBQEF2jjIzQY4+J2emmnj5k6DxSa\ - /jtpMHHh7pycx72nhxwtAuuBO61J2q0hPVKRA4N0lzS8Uqfk/uXqqeJz8KBEk5IWe0H\ - 5qkXRALPakQe5MF2DxwRbAgZvymlOhcauXCjrx2EYggfOkJYXg/kzZMrGtb+X360DD/\ - CAFvX29REYlNZ2hgVBU04nhVJFQ2MDV9KStBOu2gCDu2URKb5Z41f2Tm5RZ9V1vl6df\ - qJsl0Y4E1OTZptl8pzOHkJ3PNRzE9/4HDzotGhRH39H9EAX8O+z22IUxmsweKB0gVKc\ - 0AolzRiTKicD7ONzGIbhDAAA//8DAEqMkz9OAQAA\"]" + size: 376 + text: "[\"H4sIAAAAAAAAA2RPy07CQBT9l7tuaQ1R2klIFAQXaOMjNBjj4nZ6aaePmToPFJr+O2kwc\ + eHunJzHvaeHHC0C64E7rUnarSE9UpEDg3SXNLxSp+T+5eqp4nPwoESTkhZ7QfmqRdEA\ + s9qRB7kwXYPHBFsCBm/KaU6Fxq5cKOvHYRiCB86QlheD+TNkysa1v5ffrQMP8IAW9fb\ + 1ERiU1naGBUFTTieFUkVDYwNX0pK0E67aAIO7ZREpvlnjV/ZOblFn1XW+Xp1+omyXRj\ + gTU5Nmm2XynM4eQnc81HMT3/gcPOi0aFEff0f0QBfw77PbYhTGazB4oHSBUpzQCiXNG\ + JMqJwPs43MYhuEMAAD//wMASoyTP04BAAA=\"]" + textDecoded: + data: + currentUser: + avatarURL: https://lh3.googleusercontent.com/a/ACg8ocKFaqbYeuBkbj5dFEzx8bXV8a7i3sVbKCNPV7G0uyvk=s96-c + displayName: SourcegraphBot-9000 + hasVerifiedEmail: true + id: VXNlcjozNDQ1Mjc= + organizations: + nodes: [] + primaryEmail: + email: sourcegraphbot9k@gmail.com + username: sourcegraphbot9k-fnwmu cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:13 GMT - name: content-type value: application/json - name: transfer-encoding @@ -688,12 +708,12 @@ log: value: max-age=31536000; includeSubDomains; preload - name: content-encoding value: gzip - headersSize: 1333 + headersSize: 1473 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:27.796Z + startedDateTime: 2024-09-20T10:35:13.064Z time: 0 timings: blocked: -1 @@ -703,7 +723,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 84b962509b12000d0eef7c8a8fa655f3 + - _id: 1348ade04b900b76ba7f31905b57b14e _order: 0 cache: {} request: @@ -713,7 +733,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -731,7 +751,7 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 341 + headersSize: 414 httpVersion: HTTP/1.1 method: POST postData: @@ -757,19 +777,28 @@ log: value: null url: https://sourcegraph.com/.api/graphql?CurrentUserCodySubscription response: - bodySize: 231 + bodySize: 228 content: encoding: base64 mimeType: application/json - size: 231 - text: "[\"H4sIAAAAAAAAA1zMsQrC\",\"MBSF4Xc5c4U2dtBsRToIgqWtDm6xyRCoSbi5GUrJu4uC\ - oI7n5+Os0IoV5IopERnHl2joPb1ehnSPE9nA1rtXi6w4RUg0h/F4bVEgzMpBouvPKKB\ - CmJeOfK/YnOzDcoRkSqb4fHeGrNcDK+KGISFKUW/K3aaqRyFkVcmtuOFPt05/2f2vzT\ - nnJwAAAP//AwBSGHacwgAAAA==\"]" + size: 228 + text: "[\"H4sIAAAAAAAAA1zMsQrCMBSF4Xc5cwtN7GK2Ih0EwdJWB7fYZAjUJNzcDqXk3UVBRMfz8\ + 3E2GM0aasO0EFnPl2TpPYNZh+WeJnKRXfCvlljzkqDQHMbjtUWBOGsPha4/o4COcV47\ + Cr1me3IPxwmKabHF57uz5IIZWBM3DAVZybqs9qWoRymVEGonb/jTrTdfK6pfm3POTwA\ + AAP//AwAV1OF3wgAAAA==\"]" + textDecoded: + data: + currentUser: + codySubscription: + applyProRateLimits: true + currentPeriodEndAt: 2024-10-14T22:11:32Z + currentPeriodStartAt: 2024-09-14T22:11:32Z + plan: PRO + status: ACTIVE cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:13 GMT - name: content-type value: application/json - name: transfer-encoding @@ -795,12 +824,12 @@ log: value: max-age=31536000; includeSubDomains; preload - name: content-encoding value: gzip - headersSize: 1333 + headersSize: 1473 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:28.085Z + startedDateTime: 2024-09-20T10:35:13.342Z time: 0 timings: blocked: -1 @@ -810,7 +839,7 @@ log: send: 0 ssl: -1 wait: 0 - - _id: 2aa42833ae189b030c5bc322f1d27b0c + - _id: a8e7304a01b3588668cd733bbf3a3381 _order: 0 cache: {} request: @@ -820,7 +849,7 @@ log: - _fromType: array name: authorization value: token - REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d + REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d - _fromType: array name: content-type value: application/json; charset=utf-8 @@ -838,7 +867,7 @@ log: value: gzip,deflate - name: host value: sourcegraph.com - headersSize: 332 + headersSize: 405 httpVersion: HTTP/1.1 method: POST postData: @@ -858,18 +887,21 @@ log: value: null url: https://sourcegraph.com/.api/graphql?SiteProductVersion response: - bodySize: 139 + bodySize: 136 content: encoding: base64 mimeType: application/json - size: 139 - text: "[\"H4sIAAAAAAAAA6pWSkks\",\"SVSyqlYqzixJBdEFRfkppcklYalFxZn5eUpWSkaWhkZm\ - 5vFGBkYmugaWuobG8aZ65rrJZsamSUZGFuaWFqZKtbW1AAAAAP//AwA8UsjuSQAAAA==\ - \"]" + size: 136 + text: "[\"H4sIAAAAAAAAA6pWSkksSVSyqlYqzixJBdEFRfkppcklYalFxZn5eUpWSkaWhhbmFvFGB\ + kYmugaWuoaW8aZ65rrmxomJSaZmlkmGlslKtbW1AAAAAP//AwBQhF16SQAAAA==\"]" + textDecoded: + data: + site: + productVersion: 291878_2024-09-19_5.7-73aab569b19c cookies: [] headers: - name: date - value: Fri, 13 Sep 2024 12:08:28 GMT + value: Fri, 20 Sep 2024 10:35:13 GMT - name: content-type value: application/json - name: transfer-encoding @@ -895,12 +927,12 @@ log: value: max-age=31536000; includeSubDomains; preload - name: content-encoding value: gzip - headersSize: 1333 + headersSize: 1473 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-13T12:08:27.710Z + startedDateTime: 2024-09-20T10:35:12.978Z time: 0 timings: blocked: -1 diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index a2023c75dc..16d998933c 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -119,6 +119,7 @@ private constructor( extensionConfiguration = ConfigUtil.getAgentConfiguration(project), capabilities = ClientCapabilities( + authentication = ClientCapabilities.AuthenticationEnum.Enabled, edit = ClientCapabilities.EditEnum.Enabled, editWorkspace = ClientCapabilities.EditWorkspaceEnum.Enabled, codeLenses = ClientCapabilities.CodeLensesEnum.Enabled, diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt index 54a4fe2d92..650e37cd7b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt @@ -8,32 +8,46 @@ import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager import com.sourcegraph.cody.agent.protocol_generated.ProtocolCodeLens +import com.sourcegraph.config.ConfigUtil +import com.sourcegraph.utils.CodyEditorUtil import java.awt.Point -import java.net.URI typealias TaskId = String interface LensListener { - fun onLensesUpdate(uri: URI, codeLenses: List) + fun onLensesUpdate(vf: VirtualFile, codeLenses: List) } @Service(Service.Level.PROJECT) class LensesService(val project: Project) { - @Volatile private var lensGroups = mutableMapOf>() + @Volatile private var lensGroups = mutableMapOf>() private val listeners = mutableListOf() fun getTaskIdsOfFirstVisibleLens(editor: Editor): TaskId? { - val lenses = getLenses(editor).sortedBy { it.range.start.line } - val visibleArea = editor.scrollingModel.visibleArea - val startPosition = editor.xyToVisualPosition(visibleArea.location) - val endPosition = - editor.xyToVisualPosition( - Point(visibleArea.x + visibleArea.width, visibleArea.y + visibleArea.height)) - val cmd = lenses.find { it.range.start.line in (startPosition.line..endPosition.line) }?.command - val taskId = (cmd?.arguments?.firstOrNull() as com.google.gson.JsonPrimitive).asString + val lenses = + getLenses(editor) + .sortedBy { it.range.start.line } + .filter { it.command?.arguments?.isNotEmpty() == true } + + val cmd = + if (ConfigUtil.isIntegrationTestModeEnabled()) { + // Unfortunately headless mode does not seem to properly support `scrollingModel` so for + // tests we just return first available lens + lenses.firstOrNull()?.command + } else { + val visibleArea = editor.scrollingModel.visibleArea + val startPosition = editor.xyToVisualPosition(visibleArea.location) + val endPosition = + editor.xyToVisualPosition( + Point(visibleArea.x + visibleArea.width, visibleArea.y + visibleArea.height)) + lenses.find { it.range.start.line in (startPosition.line..endPosition.line) }?.command + } + + val taskId = (cmd?.arguments?.firstOrNull() as com.google.gson.JsonPrimitive?)?.asString return taskId } @@ -46,10 +60,8 @@ class LensesService(val project: Project) { } fun updateLenses(uriString: String, codeLens: List) { - val uri = URI.create(uriString) ?: return - synchronized(this) { lensGroups[uri] = codeLens } - - listeners.forEach { it.onLensesUpdate(uri, codeLens) } + val vf = CodyEditorUtil.findFileOrScratch(project, uriString) ?: return + synchronized(this) { lensGroups[vf] = codeLens } runInEdt { if (project.isDisposed) return@runInEdt @@ -60,16 +72,16 @@ class LensesService(val project: Project) { CodeVisionHost.LensInvalidateSignal( editor, EditCodeVisionProvider.allEditProviders().map { it.id })) } + listeners.forEach { it.onLensesUpdate(vf, codeLens) } } fun getLenses(editor: Editor): List { val document = editor.document val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return emptyList() - val virtualFile = file.viewProvider.virtualFile - val uri = URI.create(virtualFile.url) ?: return emptyList() + val vf = file.viewProvider.virtualFile synchronized(this) { - return lensGroups[uri] ?: emptyList() + return lensGroups[vf] ?: emptyList() } } From f5a7aa8e4982f22760174fe2f135d00a5f15c3a5 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Fri, 20 Sep 2024 12:59:11 +0200 Subject: [PATCH 4/8] Update recordings --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 2 + .../cody/util/TestingCredentials.kt | 2 +- .../recording.har.yaml | 44 +++++++++---------- 3 files changed, 24 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 c42559b933..ae47308952 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -12,12 +12,14 @@ import com.sourcegraph.cody.util.CodyIntegrationTextFixture import com.sourcegraph.cody.util.CustomJunitClassRunner import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.startsWith +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @RunWith(CustomJunitClassRunner::class) class DocumentCodeTest : CodyIntegrationTextFixture() { + @Ignore @Test fun testGetsWorkingGroupLens() { val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/TestingCredentials.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/TestingCredentials.kt index e653dd8709..8e0101be60 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/TestingCredentials.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/TestingCredentials.kt @@ -14,7 +14,7 @@ data class TestingCredentials( TestingCredentials( token = System.getenv("SRC_DOTCOM_PRO_ACCESS_TOKEN"), redactedToken = - "REDACTED_d5e0f0a37c9821e856b923fe14e67a605e3f6c0a517d5a4f46a4e35943ee0f6d", + "REDACTED_3dd704711f82a44ff6aba261b53b61a03fb8edba658774639148630d838c2d1d", serverEndpoint = ConfigUtil.DOTCOM_URL) val dotcomProUserRateLimited = TestingCredentials( 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 bab3c35e3f..5898279516 100644 --- a/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml +++ b/src/integrationTest/resources/recordings/integration-test_2927926756/recording.har.yaml @@ -48,7 +48,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:14 GMT + value: Fri, 20 Sep 2024 10:58:30 GMT - name: content-type value: text/plain; charset=utf-8 - name: transfer-encoding @@ -79,7 +79,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:13.980Z + startedDateTime: 2024-09-20T10:58:29.810Z time: 0 timings: blocked: -1 @@ -106,7 +106,7 @@ log: - name: user-agent value: JetBrains / 6.0-localbuild - name: traceparent - value: 00-3242a758b262e2c7e69b4ccdc616bf25-ffe701046f988c12-01 + value: 00-9b4e74b88c22decc5ed5bc93db05d813-cbd81cec67fc71b7-01 - name: connection value: keep-alive - name: host @@ -207,14 +207,14 @@ log: value: 6.0-localbuild url: https://sourcegraph.com/.api/completions/stream?api-version=2&client-name=jetbrains&client-version=6.0-localbuild response: - bodySize: 672 + bodySize: 790 content: mimeType: text/event-stream - size: 672 + size: 790 text: >+ event: completion - data: {"deltaText":"/**\n * Imports the necessary Java utilities, including the ArrayList data structure.\n */","stopReason":"stop_sequence"} + data: {"deltaText":"\n/**\n * Imports the necessary Java utility classes, such as the `ArrayList` implementation of the `List` interface.\n */\n","stopReason":"stop_sequence"} event: done @@ -224,15 +224,13 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:17 GMT + value: Fri, 20 Sep 2024 10:58:31 GMT - name: content-type value: text/event-stream - name: transfer-encoding value: chunked - name: connection value: keep-alive - - name: retry-after - value: "598" - name: access-control-allow-credentials value: "true" - name: access-control-allow-origin @@ -250,12 +248,12 @@ log: value: 1; mode=block - name: strict-transport-security value: max-age=31536000; includeSubDomains; preload - headersSize: 1406 + headersSize: 1299 httpVersion: HTTP/1.1 redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:16.531Z + startedDateTime: 2024-09-20T10:58:30.472Z time: 0 timings: blocked: -1 @@ -342,7 +340,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:13 GMT + value: Fri, 20 Sep 2024 10:58:28 GMT - name: content-type value: application/json - name: transfer-encoding @@ -373,7 +371,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:13.014Z + startedDateTime: 2024-09-20T10:58:28.630Z time: 0 timings: blocked: -1 @@ -448,7 +446,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:13 GMT + value: Fri, 20 Sep 2024 10:58:28 GMT - name: content-type value: application/json - name: transfer-encoding @@ -479,7 +477,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:13.047Z + startedDateTime: 2024-09-20T10:58:28.672Z time: 0 timings: blocked: -1 @@ -554,7 +552,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:13 GMT + value: Fri, 20 Sep 2024 10:58:29 GMT - name: content-type value: application/json - name: transfer-encoding @@ -585,7 +583,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:13.031Z + startedDateTime: 2024-09-20T10:58:28.652Z time: 0 timings: blocked: -1 @@ -682,7 +680,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:13 GMT + value: Fri, 20 Sep 2024 10:58:28 GMT - name: content-type value: application/json - name: transfer-encoding @@ -713,7 +711,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:13.064Z + startedDateTime: 2024-09-20T10:58:28.690Z time: 0 timings: blocked: -1 @@ -798,7 +796,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:13 GMT + value: Fri, 20 Sep 2024 10:58:29 GMT - name: content-type value: application/json - name: transfer-encoding @@ -829,7 +827,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:13.342Z + startedDateTime: 2024-09-20T10:58:29.000Z time: 0 timings: blocked: -1 @@ -901,7 +899,7 @@ log: cookies: [] headers: - name: date - value: Fri, 20 Sep 2024 10:35:13 GMT + value: Fri, 20 Sep 2024 10:58:28 GMT - name: content-type value: application/json - name: transfer-encoding @@ -932,7 +930,7 @@ log: redirectURL: "" status: 200 statusText: OK - startedDateTime: 2024-09-20T10:35:12.978Z + startedDateTime: 2024-09-20T10:58:28.590Z time: 0 timings: blocked: -1 From 48e9346d25f021131034ab5d4bbd33ec7bea20ad Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Mon, 23 Sep 2024 11:10:38 +0200 Subject: [PATCH 5/8] Re-enable disabled test --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 11 +++---- .../cody/util/CodyIntegrationTextFixture.kt | 30 +++++++++++++------ .../com/sourcegraph/cody/agent/CodyAgent.kt | 1 - .../cody/edit/lenses/LensesService.kt | 5 +++- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index ae47308952..941c2210a1 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -12,14 +12,12 @@ import com.sourcegraph.cody.util.CodyIntegrationTextFixture import com.sourcegraph.cody.util.CustomJunitClassRunner import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.startsWith -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @RunWith(CustomJunitClassRunner::class) class DocumentCodeTest : CodyIntegrationTextFixture() { - @Ignore @Test fun testGetsWorkingGroupLens() { val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID) @@ -35,7 +33,12 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { codeLenses[1].command?.command, EditCancelCodeVisionProvider.command) - runAndWaitForCleanState(EditCancelAction.ID) + // We could try to Cancel the action, but there is no guarantee we can do it before edit will + // finish. + // It is safer to just wait for edit to finish and then undo it. + waitForSuccessfulEdit() + + runAndWaitForCleanState(EditUndoAction.ID) } @Test @@ -61,7 +64,6 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { @Test fun testAccept() { - assertNoInlayShown() val codeLenses = runAndWaitForLenses(DocumentCodeAction.ID, EditAcceptAction.ID) assertNotNull("Lens group should be displayed", codeLenses.isNotEmpty()) @@ -82,6 +84,5 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { "Expected document changes to be reverted", originalDocument, myFixture.editor.document.text) - assertNoInlayShown() } } diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt index 4c605324e2..83bb2dac8e 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt @@ -25,6 +25,7 @@ import com.sourcegraph.cody.config.CodyPersistentAccountsHost import com.sourcegraph.cody.config.SourcegraphServerPath import com.sourcegraph.cody.edit.lenses.LensListener import com.sourcegraph.cody.edit.lenses.LensesService +import com.sourcegraph.cody.edit.lenses.providers.EditAcceptCodeVisionProvider import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern @@ -193,19 +194,30 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener { } } - protected fun assertNoInlayShown() { - runInEdtAndWait { - PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - assertFalse( - "Lens group inlay should NOT be displayed", - myFixture.editor.inlayModel.hasBlockElements()) - } - } - override fun onLensesUpdate(vf: VirtualFile, codeLenses: List) { synchronized(lensSubscribers) { lensSubscribers.removeAll { it(codeLenses) } } } + fun waitForSuccessfulEdit() { + var attempts = 0 + val maxAttempts = 10 + + while (attempts < maxAttempts) { + val hasAcceptLens = + LensesService.getInstance(myFixture.project).getLenses(myFixture.editor).any { + it.command?.command == EditAcceptCodeVisionProvider.command + } + + if (hasAcceptLens) break + Thread.sleep(1000) + attempts++ + } + if (attempts >= maxAttempts) { + assertTrue( + "Awaiting successful edit: No accept lens found after $maxAttempts attempts", false) + } + } + fun runAndWaitForCleanState(actionIdToRun: String) { runAndWaitForLenses(actionIdToRun) } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 16d998933c..a2023c75dc 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -119,7 +119,6 @@ private constructor( extensionConfiguration = ConfigUtil.getAgentConfiguration(project), capabilities = ClientCapabilities( - authentication = ClientCapabilities.AuthenticationEnum.Enabled, edit = ClientCapabilities.EditEnum.Enabled, editWorkspace = ClientCapabilities.EditWorkspaceEnum.Enabled, codeLenses = ClientCapabilities.CodeLensesEnum.Enabled, diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt index 650e37cd7b..359f4e0e8a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/LensesService.kt @@ -3,6 +3,7 @@ package com.sourcegraph.cody.edit.lenses import com.intellij.codeInsight.codeVision.CodeVisionHost import com.intellij.codeInsight.codeVision.CodeVisionInitializer import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor @@ -77,7 +78,9 @@ class LensesService(val project: Project) { fun getLenses(editor: Editor): List { val document = editor.document - val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return emptyList() + val file = + runReadAction { PsiDocumentManager.getInstance(project).getPsiFile(document) } + ?: return emptyList() val vf = file.viewProvider.virtualFile synchronized(this) { From 38de5806c043cd40a799643ba48b400aa6509b8a Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Mon, 23 Sep 2024 12:45:26 +0200 Subject: [PATCH 6/8] Revert cody commit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3f7182e2df..c5b387a228 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ kotlin.daemon.jvmargs=-Xmx2g -Xms500m nodeBinaries.commit=8755ae4c05fd476cd23f2972049111ba436c86d4 nodeBinaries.version=v20.12.2 cody.autocomplete.enableFormatting=true -cody.commit=5bd253c16261680490d2afb14398ff568334ffbd +cody.commit=daa1693620fdf2784ecd190dd12bb07169950a90 From e0763cadbf95009f910fe5fecb99aa0eddc09199 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Mon, 23 Sep 2024 13:07:51 +0200 Subject: [PATCH 7/8] Improve test debug logs --- .../com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt index 83bb2dac8e..a6cb6601fb 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt @@ -248,9 +248,9 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener { try { return future.get(ASYNC_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) } catch (e: Exception) { - val stackTrace = e.stackTrace.joinToString("\n") { it.toString() } + val codeLenses = LensesService.getInstance(myFixture.project).getLenses(myFixture.editor) assertTrue( - "Error while awaiting condition after action $actionIdToRun: ${e.localizedMessage}\n$stackTrace", + "Error while awaiting after action $actionIdToRun. Expected lenses: [${expectedLenses.joinToString()}], got: $codeLenses", false) throw e } From 529d88098278a0a9046e640522dad4ab434ffe06 Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Mon, 23 Sep 2024 13:31:27 +0200 Subject: [PATCH 8/8] Small fixes --- .../sourcegraph/cody/edit/DocumentCodeTest.kt | 3 +-- .../cody/util/CodyIntegrationTextFixture.kt | 26 ++++++++++--------- .../edit/lenses/EditCodeVisionProvider.kt | 3 ++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 941c2210a1..f51e81c3fe 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -34,8 +34,7 @@ class DocumentCodeTest : CodyIntegrationTextFixture() { EditCancelCodeVisionProvider.command) // We could try to Cancel the action, but there is no guarantee we can do it before edit will - // finish. - // It is safer to just wait for edit to finish and then undo it. + // finish. It is safer to just wait for edit to finish and then undo it. waitForSuccessfulEdit() runAndWaitForCleanState(EditUndoAction.ID) diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt index a6cb6601fb..8861c5a2ca 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/util/CodyIntegrationTextFixture.kt @@ -227,20 +227,22 @@ open class CodyIntegrationTextFixture : BasePlatformTestCase(), LensListener { vararg expectedLenses: String ): List { val future = CompletableFuture>() - lensSubscribers.add { codeLenses -> - val error = codeLenses.find { it.command?.command == "cody.fixup.codelens.error" } - if (error != null) { - future.completeExceptionally( - IllegalStateException("Error group shown: ${error.command?.title}")) - return@add true - } + synchronized(lensSubscribers) { + lensSubscribers.add { codeLenses -> + val error = codeLenses.find { it.command?.command == "cody.fixup.codelens.error" } + if (error != null) { + future.completeExceptionally( + IllegalStateException("Error group shown: ${error.command?.title}")) + return@add false + } - if ((expectedLenses.isEmpty() && codeLenses.isEmpty()) || - expectedLenses.all { expected -> codeLenses.any { it.command?.command == expected } }) { - future.complete(codeLenses) - return@add true + if ((expectedLenses.isEmpty() && codeLenses.isEmpty()) || + expectedLenses.all { expected -> codeLenses.any { it.command?.command == expected } }) { + future.complete(codeLenses) + return@add true + } + return@add false } - return@add false } triggerAction(actionIdToRun) diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt index 9ab765d180..aaee8577c4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/lenses/EditCodeVisionProvider.kt @@ -16,6 +16,7 @@ import com.intellij.openapi.application.runReadAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.keymap.KeymapManager import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.project.DumbAware import com.intellij.ui.JBColor import com.intellij.ui.SimpleTextAttributes import com.sourcegraph.cody.Icons @@ -45,7 +46,7 @@ abstract class EditCodeVisionProviderMetadata { } abstract class EditCodeVisionProvider(private val metadata: EditCodeVisionProviderMetadata) : - CodeVisionProvider { + CodeVisionProvider, DumbAware { override val id: String = metadata.id override val groupId: String = "EditCodeVisionProvider" override val name: String = "Cody Edit Lenses"